diff --git a/Dockerfile-ce b/Dockerfile-ce index 55ac03003..915494216 100644 --- a/Dockerfile-ce +++ b/Dockerfile-ce @@ -44,8 +44,8 @@ ARG TOOLS=" \ wget" ARG APT_MIRROR=http://mirrors.ustc.edu.cn -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ - --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \ sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ && rm -f /etc/apt/apt.conf.d/docker-clean \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ @@ -63,9 +63,9 @@ RUN --mount=type=cache,target=/root/.cache \ --mount=type=bind,source=pyproject.toml,target=/opt/jumpserver/pyproject.toml \ set -ex \ && python3 -m venv /opt/py3 \ - && . /opt/py3/bin/activate \ && pip install poetry -i ${PIP_MIRROR} \ && poetry config virtualenvs.create false \ + && . /opt/py3/bin/activate \ && poetry install FROM python:3.11-slim-bullseye @@ -75,8 +75,9 @@ ENV LANG=zh_CN.UTF-8 \ ARG DEPENDENCIES=" \ libjpeg-dev \ - libxmlsec1-openssl \ - libx11-dev" + libx11-dev \ + freerdp2-dev \ + libxmlsec1-openssl" ARG TOOLS=" \ ca-certificates \ @@ -94,8 +95,8 @@ ARG TOOLS=" \ wget" ARG APT_MIRROR=http://mirrors.ustc.edu.cn -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ - --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core-apt \ + --mount=type=cache,target=/var/lib/apt,sharing=locked,id=core-apt \ sed -i "s@http://.*.debian.org@${APT_MIRROR}@g" /etc/apt/sources.list \ && rm -f /etc/apt/apt.conf.d/docker-clean \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ @@ -118,7 +119,6 @@ ARG VERSION ENV VERSION=$VERSION VOLUME /opt/jumpserver/data -VOLUME /opt/jumpserver/logs EXPOSE 8080 diff --git a/apps/accounts/api/account/task.py b/apps/accounts/api/account/task.py index 16ea84eae..c4f6ebd9f 100644 --- a/apps/accounts/api/account/task.py +++ b/apps/accounts/api/account/task.py @@ -1,9 +1,12 @@ from rest_framework.generics import CreateAPIView -from rest_framework.response import Response from accounts import serializers -from accounts.tasks import verify_accounts_connectivity_task, push_accounts_to_assets_task +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__ = [ 'AccountsTaskCreateAPI', @@ -12,16 +15,16 @@ __all__ = [ class AccountsTaskCreateAPI(CreateAPIView): serializer_class = serializers.AccountTaskSerializer + permission_classes = (AccountTaskActionPermission,) - def check_permissions(self, request): - act = request.data.get('action') - if act == 'push': - code = 'accounts.push_account' - else: - code = 'accounts.verify_account' - has = request.user.has_perm(code) - if not has: - self.permission_denied(request) + def get_permissions(self): + act = self.request.data.get('action') + if act == 'remove': + self.permission_classes = [ + AccountTaskActionPermission, + UserConfirmation.require(ConfirmType.PASSWORD) + ] + return super().get_permissions() def perform_create(self, serializer): data = serializer.validated_data @@ -31,6 +34,10 @@ class AccountsTaskCreateAPI(CreateAPIView): 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) else: account = accounts[0] asset = account.asset @@ -43,9 +50,3 @@ class AccountsTaskCreateAPI(CreateAPIView): data["task"] = task.id setattr(serializer, '_data', data) return task - - def get_exception_handler(self): - def handler(e, context): - return Response({"error": str(e)}, status=401) - - return handler diff --git a/apps/accounts/automations/change_secret/custom/ssh/main.yml b/apps/accounts/automations/change_secret/custom/ssh/main.yml index c4381b730..8ff38475f 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/main.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/main.yml @@ -1,7 +1,6 @@ - hosts: custom gather_facts: no vars: - asset_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}" ansible_connection: local ansible_become: false @@ -9,7 +8,7 @@ - name: Test privileged account (paramiko) ssh_ping: login_host: "{{ jms_asset.address }}" - login_port: "{{ asset_port }}" + login_port: "{{ jms_asset.port }}" login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_secret_type: "{{ jms_account.secret_type }}" @@ -27,7 +26,7 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ asset_port }}" + login_port: "{{ jms_asset.port }}" login_secret_type: "{{ jms_account.secret_type }}" login_private_key_path: "{{ jms_account.private_key_path }}" become: "{{ custom_become | default(False) }}" @@ -49,7 +48,7 @@ login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ asset_port }}" + login_port: "{{ jms_asset.port }}" become: "{{ account.become.ansible_become | default(False) }}" become_method: su become_user: "{{ account.become.ansible_user | default('') }}" diff --git a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml index c46511344..7d3d0edde 100644 --- a/apps/accounts/automations/change_secret/custom/ssh/manifest.yml +++ b/apps/accounts/automations/change_secret/custom/ssh/manifest.yml @@ -6,15 +6,26 @@ category: type: - all method: change_secret +protocol: ssh params: - name: commands type: list - label: '自定义命令' + label: "{{ 'Params commands label' | trans }}" default: [ '' ] - help_text: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,
请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。
比如针对 Cisco 主机进行改密,一般需要配置五条命令:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end' + help_text: "{{ 'Params commands help text' | trans }}" i18n: SSH account change secret: - zh: 使用 SSH 命令行自定义改密 - ja: SSH コマンドライン方式でカスタムパスワード変更 - en: Custom password change by SSH command line + zh: '使用 SSH 命令行自定义改密' + ja: 'SSH コマンドライン方式でカスタムパスワード変更' + en: 'Custom password change by SSH command line' + + Params commands help text: + zh: '自定义命令中如需包含账号的 账号、密码、SSH 连接的用户密码 字段,
请使用 {username}、{password}、{login_password}格式,执行任务时会进行替换 。
比如针对 Cisco 主机进行改密,一般需要配置五条命令:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end' + ja: 'カスタム コマンドに SSH 接続用のアカウント番号、パスワード、ユーザー パスワード フィールドを含める必要がある場合は、
{ユーザー名}、{パスワード}、{login_password& を使用してください。 # 125; 形式。タスクの実行時に置き換えられます。
たとえば、Cisco ホストのパスワードを変更するには、通常、次の 5 つのコマンドを設定する必要があります:
1.enable
2.{login_password}
3 .ターミナルの設定
4. ユーザー名 {ユーザー名} 権限 0 パスワード {パスワード}
5. 終了' + en: 'If the custom command needs to include the account number, password, and user password field for SSH connection,
Please use {username}, {password}, {login_password&# 125; format, which will be replaced when executing the task.
For example, to change the password of a Cisco host, you generally need to configure five commands:
1. enable
2. {login_password}
3. configure terminal
4. username {username} privilege 0 password {password}
5. end' + + Params commands label: + zh: '自定义命令' + ja: 'カスタムコマンド' + en: 'Custom command' diff --git a/apps/accounts/automations/change_secret/database/mysql/main.yml b/apps/accounts/automations/change_secret/database/mysql/main.yml index 91b7d6f2c..f36eff171 100644 --- a/apps/accounts/automations/change_secret/database/mysql/main.yml +++ b/apps/accounts/automations/change_secret/database/mysql/main.yml @@ -3,6 +3,7 @@ vars: ansible_python_interpreter: /opt/py3/bin/python db_name: "{{ jms_asset.spec_info.db_name }}" + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Test MySQL connection @@ -11,10 +12,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version register: db_info @@ -28,10 +29,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" name: "{{ account.username }}" password: "{{ account.secret }}" host: "%" @@ -45,8 +46,8 @@ login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version diff --git a/apps/accounts/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py index adc840c3f..46d3ef2b6 100644 --- a/apps/accounts/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -139,7 +139,7 @@ class ChangeSecretManager(AccountBasePlaybookManager): 'name': account.name, 'username': account.username, 'secret_type': secret_type, - 'secret': new_secret, + 'secret': account.escape_jinja2_syntax(new_secret), 'private_key_path': private_key_path, 'become': account.get_ansible_become_auth(), } diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py index c1a19c968..f045858a7 100644 --- a/apps/accounts/automations/endpoint.py +++ b/apps/accounts/automations/endpoint.py @@ -1,8 +1,9 @@ -from .push_account.manager import PushAccountManager -from .change_secret.manager import ChangeSecretManager -from .verify_account.manager import VerifyAccountManager from .backup_account.manager import AccountBackupManager +from .change_secret.manager import ChangeSecretManager from .gather_accounts.manager import GatherAccountsManager +from .push_account.manager import PushAccountManager +from .remove_account.manager import RemoveAccountManager +from .verify_account.manager import VerifyAccountManager from .verify_gateway_account.manager import VerifyGatewayAccountManager from ..const import AutomationTypes @@ -12,6 +13,7 @@ class ExecutionManager: AutomationTypes.push_account: PushAccountManager, AutomationTypes.change_secret: ChangeSecretManager, AutomationTypes.verify_account: VerifyAccountManager, + AutomationTypes.remove_account: RemoveAccountManager, AutomationTypes.gather_accounts: GatherAccountsManager, AutomationTypes.verify_gateway_account: VerifyGatewayAccountManager, # TODO 后期迁移到自动化策略中 diff --git a/apps/accounts/automations/gather_accounts/database/mysql/main.yml b/apps/accounts/automations/gather_accounts/database/mysql/main.yml index e8cf3cac4..e36925209 100644 --- a/apps/accounts/automations/gather_accounts/database/mysql/main.yml +++ b/apps/accounts/automations/gather_accounts/database/mysql/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_python_interpreter: /opt/py3/bin/python + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Get info @@ -10,10 +11,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: users register: db_info diff --git a/apps/accounts/automations/push_account/database/mysql/main.yml b/apps/accounts/automations/push_account/database/mysql/main.yml index 91b7d6f2c..f36eff171 100644 --- a/apps/accounts/automations/push_account/database/mysql/main.yml +++ b/apps/accounts/automations/push_account/database/mysql/main.yml @@ -3,6 +3,7 @@ vars: ansible_python_interpreter: /opt/py3/bin/python db_name: "{{ jms_asset.spec_info.db_name }}" + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Test MySQL connection @@ -11,10 +12,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version register: db_info @@ -28,10 +29,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" name: "{{ account.username }}" password: "{{ account.secret }}" host: "%" @@ -45,8 +46,8 @@ login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version diff --git a/apps/accounts/automations/push_account/host/aix/manifest.yml b/apps/accounts/automations/push_account/host/aix/manifest.yml index 949d49758..ee62d7020 100644 --- a/apps/accounts/automations/push_account/host/aix/manifest.yml +++ b/apps/accounts/automations/push_account/host/aix/manifest.yml @@ -9,7 +9,7 @@ params: type: str label: 'Sudo' default: '/bin/whoami' - help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + help_text: "{{ 'Params sudo help text' | trans }}" - name: shell type: str @@ -18,19 +18,44 @@ params: - name: home type: str - label: '家目录' + label: "{{ 'Params home label' | trans }}" default: '' - help_text: '默认家目录 /home/系统用户名: /home/username' + help_text: "{{ 'Params home help text' | trans }}" - name: groups type: str - label: '用户组' + label: "{{ 'Params groups label' | trans }}" default: '' - help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + help_text: "{{ 'Params groups help text' | trans }}" i18n: Aix account push: - zh: 使用 Ansible 模块 user 执行 Aix 账号推送 (DES) - ja: Ansible user モジュールを使用して Aix アカウントをプッシュする (DES) - en: Using Ansible module user to push account (DES) + zh: '使用 Ansible 模块 user 执行 Aix 账号推送 (DES)' + ja: 'Ansible user モジュールを使用して Aix アカウントをプッシュする (DES)' + en: 'Using Ansible module user to push account (DES)' + + Params sudo help text: + zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' + en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig' + + Params home help text: + zh: '默认家目录 /home/{账号用户名}' + ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' + en: 'Default home directory /home/{account username}' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' + + Params home label: + zh: '家目录' + ja: 'ホームディレクトリ' + en: 'Home' + + Params groups label: + zh: '用户组' + ja: 'グループ' + en: 'Groups' diff --git a/apps/accounts/automations/push_account/host/posix/manifest.yml b/apps/accounts/automations/push_account/host/posix/manifest.yml index 0c1d31845..32964f1d6 100644 --- a/apps/accounts/automations/push_account/host/posix/manifest.yml +++ b/apps/accounts/automations/push_account/host/posix/manifest.yml @@ -10,7 +10,7 @@ params: type: str label: 'Sudo' default: '/bin/whoami' - help_text: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + help_text: "{{ 'Params sudo help text' | trans }}" - name: shell type: str @@ -20,18 +20,43 @@ params: - name: home type: str - label: '家目录' + label: "{{ 'Params home label' | trans }}" default: '' - help_text: '默认家目录 /home/系统用户名: /home/username' + help_text: "{{ 'Params home help text' | trans }}" - name: groups type: str - label: '用户组' + label: "{{ 'Params groups label' | trans }}" default: '' - help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + help_text: "{{ 'Params groups help text' | trans }}" i18n: Posix account push: - zh: 使用 Ansible 模块 user 执行账号推送 (sha512) - ja: Ansible user モジュールを使用してアカウントをプッシュする (sha512) - en: Using Ansible module user to push account (sha512) + zh: '使用 Ansible 模块 user 执行账号推送 (sha512)' + ja: 'Ansible user モジュールを使用してアカウントをプッシュする (sha512)' + en: 'Using Ansible module user to push account (sha512)' + + Params sudo help text: + zh: '使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig' + ja: 'コンマで区切って複数のコマンドを入力してください。例: /bin/whoami,/sbin/ifconfig' + en: 'Use commas to separate multiple commands, such as: /bin/whoami,/sbin/ifconfig' + + Params home help text: + zh: '默认家目录 /home/{账号用户名}' + ja: 'デフォルトのホームディレクトリ /home/{アカウントユーザ名}' + en: 'Default home directory /home/{account username}' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' + + Params home label: + zh: '家目录' + ja: 'ホームディレクトリ' + en: 'Home' + + Params groups label: + zh: '用户组' + ja: 'グループ' + en: 'Groups' \ No newline at end of file diff --git a/apps/accounts/automations/push_account/host/windows/manifest.yml b/apps/accounts/automations/push_account/host/windows/manifest.yml index 7866e3c13..dcbdfe7f8 100644 --- a/apps/accounts/automations/push_account/host/windows/manifest.yml +++ b/apps/accounts/automations/push_account/host/windows/manifest.yml @@ -10,10 +10,15 @@ params: type: str label: '用户组' default: 'Users,Remote Desktop Users' - help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + help_text: "{{ 'Params groups help text' | trans }}" i18n: Windows account push: - zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送 - ja: Ansible win_user モジュールを使用して Windows アカウントをプッシュする - en: Using Ansible module win_user to push account + zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送' + ja: 'Ansible win_user モジュールを使用して Windows アカウントをプッシュする' + en: 'Using Ansible module win_user to push account' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' diff --git a/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml b/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml index 449cf726f..d08a29ebc 100644 --- a/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml +++ b/apps/accounts/automations/push_account/host/windows_rdp_verify/manifest.yml @@ -10,10 +10,15 @@ params: type: str label: '用户组' default: 'Users,Remote Desktop Users' - help_text: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + help_text: "{{ 'Params groups help text' | trans }}" i18n: Windows account push rdp verify: - zh: 使用 Ansible 模块 win_user 执行 Windows 账号推送 RDP 协议测试最后的可连接性 - ja: Ansibleモジュールwin_userがWindowsアカウントプッシュRDPプロトコルテストを実行する最後の接続性 - en: Using the Ansible module win_user performs Windows account push RDP protocol testing for final connectivity + zh: '使用 Ansible 模块 win_user 执行 Windows 账号推送(最后使用 Python 模块 pyfreerdp 验证账号的可连接性)' + ja: 'Ansible モジュール win_user を使用して Windows アカウントのプッシュを実行します (最後に Python モジュール pyfreerdp を使用してアカウントの接続性を確認します)' + en: 'Use the Ansible module win_user to perform Windows account push (finally use the Python module pyfreerdp to verify the connectability of the account)' + + Params groups help text: + zh: '请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)' + ja: 'グループを入力してください。複数のグループはコンマで区切ってください(既存のグループを入力してください)' + en: 'Please enter the group. Multiple groups are separated by commas (please enter the existing group)' diff --git a/apps/accounts/automations/remove_account/__init__.py b/apps/accounts/automations/remove_account/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/accounts/automations/remove_account/database/mongodb/main.yml b/apps/accounts/automations/remove_account/database/mongodb/main.yml new file mode 100644 index 000000000..3ec800981 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/mongodb/main.yml @@ -0,0 +1,21 @@ +- hosts: mongodb + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: "Remove account" + mongodb_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + ssl: "{{ jms_asset.spec_info.use_ssl }}" + ssl_ca_certs: "{{ jms_asset.secret_info.ca_cert | default('') }}" + ssl_certfile: "{{ jms_asset.secret_info.client_key | default('') }}" + connection_options: + - tlsAllowInvalidHostnames: "{{ jms_asset.spec_info.allow_invalid_cert}}" + db: "{{ jms_asset.spec_info.db_name }}" + name: "{{ account.username }}" + state: absent \ No newline at end of file diff --git a/apps/accounts/automations/remove_account/database/mongodb/manifest.yml b/apps/accounts/automations/remove_account/database/mongodb/manifest.yml new file mode 100644 index 000000000..681a6a521 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/mongodb/manifest.yml @@ -0,0 +1,12 @@ +id: remove_account_mongodb +name: "{{ 'MongoDB account remove' | trans }}" +category: database +type: + - mongodb +method: remove_account + +i18n: + MongoDB account remove: + zh: 使用 Ansible 模块 mongodb 删除账号 + ja: Ansible モジュール mongodb を使用してアカウントを削除する + en: Delete account using Ansible module mongodb diff --git a/apps/accounts/automations/remove_account/database/mysql/main.yml b/apps/accounts/automations/remove_account/database/mysql/main.yml new file mode 100644 index 000000000..563a7d74b --- /dev/null +++ b/apps/accounts/automations/remove_account/database/mysql/main.yml @@ -0,0 +1,18 @@ +- hosts: mysql + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: "Remove account" + community.mysql.mysql_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" + name: "{{ account.username }}" + state: absent diff --git a/apps/accounts/automations/remove_account/database/mysql/manifest.yml b/apps/accounts/automations/remove_account/database/mysql/manifest.yml new file mode 100644 index 000000000..ff8845a6d --- /dev/null +++ b/apps/accounts/automations/remove_account/database/mysql/manifest.yml @@ -0,0 +1,14 @@ +id: remove_account_mysql +name: "{{ 'MySQL account remove' | trans }}" +category: database +type: + - mysql + - mariadb +method: remove_account + +i18n: + MySQL account remove: + zh: 使用 Ansible 模块 mysql_user 删除账号 + ja: Ansible モジュール mysql_user を使用してアカウントを削除します + en: Use the Ansible module mysql_user to delete the account + diff --git a/apps/accounts/automations/remove_account/database/oracle/main.yml b/apps/accounts/automations/remove_account/database/oracle/main.yml new file mode 100644 index 000000000..ffd846d47 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/oracle/main.yml @@ -0,0 +1,16 @@ +- hosts: oracle + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: "Remove account" + oracle_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + login_database: "{{ jms_asset.spec_info.db_name }}" + mode: "{{ jms_account.mode }}" + name: "{{ account.username }}" + state: absent diff --git a/apps/accounts/automations/remove_account/database/oracle/manifest.yml b/apps/accounts/automations/remove_account/database/oracle/manifest.yml new file mode 100644 index 000000000..173053673 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/oracle/manifest.yml @@ -0,0 +1,12 @@ +id: remove_account_oracle +name: "{{ 'Oracle account remove' | trans }}" +category: database +type: + - oracle +method: remove_account + +i18n: + Oracle account remove: + zh: 使用 Python 模块 oracledb 删除账号 + ja: Python モジュール oracledb を使用してアカウントを検証する + en: Using Python module oracledb to verify account diff --git a/apps/accounts/automations/remove_account/database/postgresql/main.yml b/apps/accounts/automations/remove_account/database/postgresql/main.yml new file mode 100644 index 000000000..7004dc945 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/postgresql/main.yml @@ -0,0 +1,15 @@ +- hosts: postgresql + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: "Remove account" + community.postgresql.postgresql_user: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + db: "{{ jms_asset.spec_info.db_name }}" + name: "{{ account.username }}" + state: absent diff --git a/apps/accounts/automations/remove_account/database/postgresql/manifest.yml b/apps/accounts/automations/remove_account/database/postgresql/manifest.yml new file mode 100644 index 000000000..c6c143e94 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/postgresql/manifest.yml @@ -0,0 +1,12 @@ +id: remove_account_postgresql +name: "{{ 'PostgreSQL account remove' | trans }}" +category: database +type: + - postgresql +method: remove_account + +i18n: + PostgreSQL account remove: + zh: 使用 Ansible 模块 postgresql_user 删除账号 + ja: Ansible モジュール postgresql_user を使用してアカウントを削除します + en: Use the Ansible module postgresql_user to delete the account diff --git a/apps/accounts/automations/remove_account/database/sqlserver/main.yml b/apps/accounts/automations/remove_account/database/sqlserver/main.yml new file mode 100644 index 000000000..597e12906 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/sqlserver/main.yml @@ -0,0 +1,14 @@ +- hosts: sqlserver + gather_facts: no + vars: + ansible_python_interpreter: /opt/py3/bin/python + + tasks: + - name: "Remove account" + community.general.mssql_script: + login_user: "{{ jms_account.username }}" + login_password: "{{ jms_account.secret }}" + login_host: "{{ jms_asset.address }}" + login_port: "{{ jms_asset.port }}" + name: "{{ jms_asset.spec_info.db_name }}" + script: "DROP USER {{ account.username }}" diff --git a/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml b/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml new file mode 100644 index 000000000..33c4895e2 --- /dev/null +++ b/apps/accounts/automations/remove_account/database/sqlserver/manifest.yml @@ -0,0 +1,12 @@ +id: remove_account_sqlserver +name: "{{ 'SQLServer account remove' | trans }}" +category: database +type: + - sqlserver +method: remove_account + +i18n: + SQLServer account remove: + zh: 使用 Ansible 模块 mssql 删除账号 + ja: Ansible モジュール mssql を使用してアカウントを削除する + en: Use Ansible module mssql to delete account diff --git a/apps/accounts/automations/remove_account/host/posix/main.yml b/apps/accounts/automations/remove_account/host/posix/main.yml new file mode 100644 index 000000000..de91b8552 --- /dev/null +++ b/apps/accounts/automations/remove_account/host/posix/main.yml @@ -0,0 +1,25 @@ +- hosts: demo + gather_facts: no + tasks: + - name: "Get user home directory path" + ansible.builtin.shell: + cmd: "getent passwd {{ account.username }} | cut -d: -f6" + register: user_home_dir + ignore_errors: yes + + - name: "Check if user home directory exists" + ansible.builtin.stat: + path: "{{ user_home_dir.stdout }}" + register: home_dir + when: user_home_dir.stdout != "" + + - name: "Rename user home directory if it exists" + ansible.builtin.command: + cmd: "mv {{ user_home_dir.stdout }} {{ user_home_dir.stdout }}.bak" + when: home_dir.stat.exists and user_home_dir.stdout != "" + + - name: "Remove account" + ansible.builtin.user: + name: "{{ account.username }}" + state: absent + remove: "{{ home_dir.stat.exists }}" diff --git a/apps/accounts/automations/remove_account/host/posix/manifest.yml b/apps/accounts/automations/remove_account/host/posix/manifest.yml new file mode 100644 index 000000000..753c63574 --- /dev/null +++ b/apps/accounts/automations/remove_account/host/posix/manifest.yml @@ -0,0 +1,13 @@ +id: remove_account_posix +name: "{{ 'Posix account remove' | trans }}" +category: host +type: + - linux + - unix +method: remove_account + +i18n: + Posix account remove: + zh: 使用 Ansible 模块 user 删除账号 + ja: Ansible モジュール ユーザーを使用してアカウントを削除します + en: Use the Ansible module user to delete the account diff --git a/apps/accounts/automations/remove_account/host/windows/main.yml b/apps/accounts/automations/remove_account/host/windows/main.yml new file mode 100644 index 000000000..7be9940b3 --- /dev/null +++ b/apps/accounts/automations/remove_account/host/windows/main.yml @@ -0,0 +1,9 @@ +- hosts: windows + gather_facts: no + tasks: + - name: "Remove account" + ansible.windows.win_user: + name: "{{ account.username }}" + state: absent + purge: yes + force: yes \ No newline at end of file diff --git a/apps/accounts/automations/remove_account/host/windows/manifest.yml b/apps/accounts/automations/remove_account/host/windows/manifest.yml new file mode 100644 index 000000000..588d7efea --- /dev/null +++ b/apps/accounts/automations/remove_account/host/windows/manifest.yml @@ -0,0 +1,13 @@ +id: remove_account_windows +name: "{{ 'Windows account remove' | trans }}" +version: 1 +method: remove_account +category: host +type: + - windows + +i18n: + Windows account remove: + zh: 使用 Ansible 模块 win_user 删除账号 + ja: Ansible モジュール win_user を使用してアカウントを削除する + en: Use the Ansible module win_user to delete an account diff --git a/apps/accounts/automations/remove_account/manager.py b/apps/accounts/automations/remove_account/manager.py new file mode 100644 index 000000000..37dd28f2d --- /dev/null +++ b/apps/accounts/automations/remove_account/manager.py @@ -0,0 +1,67 @@ +import os +from copy import deepcopy + +from django.db.models import QuerySet + +from accounts.const import AutomationTypes +from accounts.models import Account +from common.utils import get_logger +from ..base.manager import AccountBasePlaybookManager + +logger = get_logger(__name__) + + +class RemoveAccountManager(AccountBasePlaybookManager): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.host_account_mapper = {} + + def prepare_runtime_dir(self): + path = super().prepare_runtime_dir() + ansible_config_path = os.path.join(path, 'ansible.cfg') + + with open(ansible_config_path, 'w') as f: + f.write('[ssh_connection]\n') + f.write('ssh_args = -o ControlMaster=no -o ControlPersist=no\n') + return path + + @classmethod + def method_type(cls): + return AutomationTypes.remove_account + + def get_gather_accounts(self, privilege_account, gather_accounts: QuerySet): + gather_account_ids = self.execution.snapshot['gather_accounts'] + gather_accounts = gather_accounts.filter(id__in=gather_account_ids) + gather_accounts = gather_accounts.exclude( + username__in=[privilege_account.username, 'root', 'Administrator'] + ) + return gather_accounts + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): + if host.get('error'): + return host + + gather_accounts = asset.gatheredaccount_set.all() + gather_accounts = self.get_gather_accounts(account, gather_accounts) + + inventory_hosts = [] + + for gather_account in gather_accounts: + h = deepcopy(host) + h['name'] += '(' + gather_account.username + ')' + self.host_account_mapper[h['name']] = (asset, gather_account) + h['account'] = {'username': gather_account.username} + inventory_hosts.append(h) + return inventory_hosts + + def on_host_success(self, host, result): + tuple_asset_gather_account = self.host_account_mapper.get(host) + if not tuple_asset_gather_account: + return + asset, gather_account = tuple_asset_gather_account + Account.objects.filter( + asset_id=asset.id, + username=gather_account.username + ).delete() + gather_account.delete() diff --git a/apps/accounts/automations/verify_account/custom/rdp/main.yml b/apps/accounts/automations/verify_account/custom/rdp/main.yml index 017f4bab3..b0c7cbe4f 100644 --- a/apps/accounts/automations/verify_account/custom/rdp/main.yml +++ b/apps/accounts/automations/verify_account/custom/rdp/main.yml @@ -8,7 +8,7 @@ - name: Verify account (pyfreerdp) rdp_ping: login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" + login_port: "{{ jms_asset.port }}" login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" diff --git a/apps/accounts/automations/verify_account/custom/rdp/manifest.yml b/apps/accounts/automations/verify_account/custom/rdp/manifest.yml index 79fcce96b..e4b034366 100644 --- a/apps/accounts/automations/verify_account/custom/rdp/manifest.yml +++ b/apps/accounts/automations/verify_account/custom/rdp/manifest.yml @@ -5,9 +5,10 @@ category: type: - windows method: verify_account +protocol: rdp i18n: Windows rdp account verify: - zh: 使用 Python 模块 pyfreerdp 验证账号 - ja: Python モジュール pyfreerdp を使用してアカウントを検証する - en: Using Python module pyfreerdp to verify account + zh: '使用 Python 模块 pyfreerdp 验证账号' + ja: 'Python モジュール pyfreerdp を使用してアカウントを検証する' + en: 'Using Python module pyfreerdp to verify account' diff --git a/apps/accounts/automations/verify_account/custom/ssh/main.yml b/apps/accounts/automations/verify_account/custom/ssh/main.yml index 4519fc3ad..05be21f0c 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/main.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/main.yml @@ -9,7 +9,7 @@ - name: Verify account (paramiko) ssh_ping: login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}" + login_port: "{{ jms_asset.port }}" login_user: "{{ account.username }}" login_password: "{{ account.secret }}" login_secret_type: "{{ account.secret_type }}" diff --git a/apps/accounts/automations/verify_account/custom/ssh/manifest.yml b/apps/accounts/automations/verify_account/custom/ssh/manifest.yml index 666266416..bebc02c7f 100644 --- a/apps/accounts/automations/verify_account/custom/ssh/manifest.yml +++ b/apps/accounts/automations/verify_account/custom/ssh/manifest.yml @@ -6,9 +6,10 @@ category: type: - all method: verify_account +protocol: ssh i18n: SSH account verify: - zh: 使用 Python 模块 paramiko 验证账号 - ja: Python モジュール paramiko を使用してアカウントを検証する - en: Using Python module paramiko to verify account + zh: '使用 Python 模块 paramiko 验证账号' + ja: 'Python モジュール paramiko を使用してアカウントを検証する' + en: 'Using Python module paramiko to verify account' diff --git a/apps/accounts/automations/verify_account/database/mongodb/main.yml b/apps/accounts/automations/verify_account/database/mongodb/main.yml index 2770e6eb4..13ecccb61 100644 --- a/apps/accounts/automations/verify_account/database/mongodb/main.yml +++ b/apps/accounts/automations/verify_account/database/mongodb/main.yml @@ -1,4 +1,4 @@ -- hosts: mongdb +- hosts: mongodb gather_facts: no vars: ansible_python_interpreter: /opt/py3/bin/python diff --git a/apps/accounts/automations/verify_account/database/mysql/main.yml b/apps/accounts/automations/verify_account/database/mysql/main.yml index 2ae3a4abd..e2768d2c2 100644 --- a/apps/accounts/automations/verify_account/database/mysql/main.yml +++ b/apps/accounts/automations/verify_account/database/mysql/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_python_interpreter: /opt/py3/bin/python + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Verify account @@ -10,8 +11,8 @@ login_password: "{{ account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version diff --git a/apps/accounts/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py index 18478fb21..794cf4ff6 100644 --- a/apps/accounts/automations/verify_account/manager.py +++ b/apps/accounts/automations/verify_account/manager.py @@ -62,7 +62,7 @@ class VerifyAccountManager(AccountBasePlaybookManager): 'name': account.name, 'username': account.username, 'secret_type': account.secret_type, - 'secret': secret, + 'secret': account.escape_jinja2_syntax(secret), 'private_key_path': private_key_path, 'become': account.get_ansible_become_auth(), } diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py index 0a67de5c8..cde7fe982 100644 --- a/apps/accounts/const/automation.py +++ b/apps/accounts/const/automation.py @@ -24,6 +24,7 @@ class AutomationTypes(models.TextChoices): push_account = 'push_account', _('Push account') change_secret = 'change_secret', _('Change secret') verify_account = 'verify_account', _('Verify account') + remove_account = 'remove_account', _('Remove account') gather_accounts = 'gather_accounts', _('Gather accounts') verify_gateway_account = 'verify_gateway_account', _('Verify gateway account') diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py index e71a5b9fb..5e4e0c257 100644 --- a/apps/accounts/filters.py +++ b/apps/accounts/filters.py @@ -51,6 +51,7 @@ class AccountFilterSet(BaseFilterSet): class GatheredAccountFilterSet(BaseFilterSet): node_id = drf_filters.CharFilter(method='filter_nodes') + asset_id = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') @staticmethod def filter_nodes(queryset, name, value): @@ -58,4 +59,4 @@ class GatheredAccountFilterSet(BaseFilterSet): class Meta: model = GatheredAccount - fields = ['id', 'asset_id', 'username'] + fields = ['id', 'username'] diff --git a/apps/accounts/migrations/0007_alter_account_options.py b/apps/accounts/migrations/0007_alter_account_options.py index 4ec798a21..6def6de6e 100644 --- a/apps/accounts/migrations/0007_alter_account_options.py +++ b/apps/accounts/migrations/0007_alter_account_options.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('accounts', '0006_gatheredaccount'), ] @@ -12,6 +11,13 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='account', - options={'permissions': [('view_accountsecret', 'Can view asset account secret'), ('view_historyaccount', 'Can view asset history account'), ('view_historyaccountsecret', 'Can view asset history account secret'), ('verify_account', 'Can verify account'), ('push_account', 'Can push account')], 'verbose_name': 'Account'}, + options={'permissions': [ + ('view_accountsecret', 'Can view asset account secret'), + ('view_historyaccount', 'Can view asset history account'), + ('view_historyaccountsecret', 'Can view asset history account secret'), + ('verify_account', 'Can verify account'), + ('push_account', 'Can push account'), + ('remove_account', 'Can remove account'), + ], 'verbose_name': 'Account'}, ), ] diff --git a/apps/accounts/models/account.py b/apps/accounts/models/account.py index 8a9187190..4a175609f 100644 --- a/apps/accounts/models/account.py +++ b/apps/accounts/models/account.py @@ -4,6 +4,7 @@ from simple_history.models import HistoricalRecords from assets.models.base import AbsConnectivity from common.utils import lazyproperty +from labels.mixins import LabeledMixin from .base import BaseAccount from .mixins import VaultModelMixin from ..const import Source @@ -42,7 +43,7 @@ class AccountHistoricalRecords(HistoricalRecords): return super().create_history_model(model, inherited) -class Account(AbsConnectivity, BaseAccount): +class Account(AbsConnectivity, LabeledMixin, BaseAccount): asset = models.ForeignKey( 'assets.Asset', related_name='accounts', on_delete=models.CASCADE, verbose_name=_('Asset') @@ -68,10 +69,15 @@ class Account(AbsConnectivity, BaseAccount): ('view_historyaccountsecret', _('Can view asset history account secret')), ('verify_account', _('Can verify account')), ('push_account', _('Can push account')), + ('remove_account', _('Can remove account')), ] def __str__(self): - return '{}'.format(self.username) + if self.asset_id: + host = self.asset.name + else: + host = 'Dynamic' + return '{}({})'.format(self.name, host) @lazyproperty def platform(self): @@ -95,14 +101,13 @@ class Account(AbsConnectivity, BaseAccount): """ 排除自己和以自己为 su-from 的账号 """ return self.asset.accounts.exclude(id=self.id).exclude(su_from=self) - @staticmethod - def make_account_ansible_vars(su_from): + def make_account_ansible_vars(self, su_from): var = { 'ansible_user': su_from.username, } if not su_from.secret: return var - var['ansible_password'] = su_from.secret + var['ansible_password'] = self.escape_jinja2_syntax(su_from.secret) var['ansible_ssh_private_key_file'] = su_from.private_key_path return var @@ -119,9 +124,25 @@ class Account(AbsConnectivity, BaseAccount): auth['ansible_become'] = True auth['ansible_become_method'] = become_method auth['ansible_become_user'] = self.username - auth['ansible_become_password'] = password + auth['ansible_become_password'] = self.escape_jinja2_syntax(password) return auth + @staticmethod + def escape_jinja2_syntax(value): + if not isinstance(value, str): + return value + + def escape(v): + v = v.replace('{{', '__TEMP_OPEN_BRACES__') \ + .replace('}}', '__TEMP_CLOSE_BRACES__') + + v = v.replace('__TEMP_OPEN_BRACES__', '{{ "{{" }}') \ + .replace('__TEMP_CLOSE_BRACES__', '{{ "}}" }}') + + return v.replace('{%', '{{ "{%" }}').replace('%}', '{{ "%}" }}') + + return escape(value) + def replace_history_model_with_mixin(): """ diff --git a/apps/accounts/models/template.py b/apps/accounts/models/template.py index c56be1464..63ed1b20d 100644 --- a/apps/accounts/models/template.py +++ b/apps/accounts/models/template.py @@ -3,13 +3,14 @@ from django.db.models import Count, Q from django.utils import timezone from django.utils.translation import gettext_lazy as _ +from labels.mixins import LabeledMixin from .account import Account from .base import BaseAccount, SecretWithRandomMixin __all__ = ['AccountTemplate', ] -class AccountTemplate(BaseAccount, SecretWithRandomMixin): +class AccountTemplate(LabeledMixin, BaseAccount, SecretWithRandomMixin): su_from = models.ForeignKey( 'self', related_name='su_to', null=True, on_delete=models.SET_NULL, verbose_name=_("Su from") diff --git a/apps/accounts/notifications.py b/apps/accounts/notifications.py index b650410d1..404125fd3 100644 --- a/apps/accounts/notifications.py +++ b/apps/accounts/notifications.py @@ -3,8 +3,8 @@ from django.utils.translation import gettext_lazy as _ from common.tasks import send_mail_attachment_async, upload_backup_to_obj_storage from notifications.notifications import UserMessage -from users.models import User from terminal.models.component.storage import ReplayStorage +from users.models import User class AccountBackupExecutionTaskMsg(object): @@ -23,8 +23,8 @@ class AccountBackupExecutionTaskMsg(object): else: return _("{} - The account backup passage 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) + "please go to personal information -> Basic file encryption password for preference settings" + ).format(name) def publish(self, attachment_list=None): send_mail_attachment_async( diff --git a/apps/accounts/permissions.py b/apps/accounts/permissions.py new file mode 100644 index 000000000..6d3b94260 --- /dev/null +++ b/apps/accounts/permissions.py @@ -0,0 +1,19 @@ +from rest_framework import permissions + + +def check_permissions(request): + act = request.data.get('action') + if act == 'push': + code = 'accounts.push_account' + elif act == 'remove': + code = 'accounts.remove_account' + else: + code = 'accounts.verify_account' + return request.user.has_perm(code) + + +class AccountTaskActionPermission(permissions.IsAuthenticated): + + def has_permission(self, request, view): + return super().has_permission(request, view) \ + and check_permissions(request) diff --git a/apps/accounts/serializers/account/account.py b/apps/accounts/serializers/account/account.py index 8b8d9be36..197c64c7d 100644 --- a/apps/accounts/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -10,7 +10,7 @@ from rest_framework.generics import get_object_or_404 from rest_framework.validators import UniqueTogetherValidator from accounts.const import SecretType, Source, AccountInvalidPolicy -from accounts.models import Account, AccountTemplate +from accounts.models import Account, AccountTemplate, GatheredAccount from accounts.tasks import push_accounts_to_assets_task from assets.const import Category, AllTypes from assets.models import Asset @@ -66,6 +66,9 @@ class AccountCreateUpdateSerializerMixin(serializers.Serializer): name = initial_data.get('name') if name is not None: return + request = self.context.get('request') + if request and request.method == 'PATCH': + return if not name: name = initial_data.get('username') if self.instance and self.instance.name == name: @@ -238,7 +241,7 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize queryset = queryset.prefetch_related( 'asset', 'asset__platform', 'asset__platform__automation' - ) + ).prefetch_related('labels', 'labels__label') return queryset @@ -455,11 +458,15 @@ class AccountTaskSerializer(serializers.Serializer): ('test', 'test'), ('verify', 'verify'), ('push', 'push'), + ('remove', 'remove'), ) action = serializers.ChoiceField(choices=ACTION_CHOICES, write_only=True) accounts = serializers.PrimaryKeyRelatedField( queryset=Account.objects, required=False, allow_empty=True, many=True ) + gather_accounts = serializers.PrimaryKeyRelatedField( + queryset=GatheredAccount.objects, required=False, allow_empty=True, many=True + ) task = serializers.CharField(read_only=True) params = serializers.JSONField( decoder=None, encoder=None, required=False, diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py index 5289ea25b..23dec0d3e 100644 --- a/apps/accounts/serializers/account/base.py +++ b/apps/accounts/serializers/account/base.py @@ -5,6 +5,7 @@ from rest_framework import serializers from accounts.const import SecretType from accounts.models import BaseAccount from accounts.utils import validate_password_for_ansible, validate_ssh_key +from common.serializers import ResourceLabelsMixin from common.serializers.fields import EncryptedField, LabeledChoiceField from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -60,8 +61,7 @@ class AuthValidateMixin(serializers.Serializer): return super().update(instance, validated_data) -class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): - +class BaseAccountSerializer(AuthValidateMixin, ResourceLabelsMixin, BulkOrgResourceModelSerializer): class Meta: model = BaseAccount fields_mini = ['id', 'name', 'username'] @@ -70,7 +70,7 @@ class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): 'privileged', 'is_active', 'spec_info', ] fields_other = ['created_by', 'date_created', 'date_updated', 'comment'] - fields = fields_small + fields_other + fields = fields_small + fields_other + ['labels'] read_only_fields = [ 'spec_info', 'date_verified', 'created_by', 'date_created', ] diff --git a/apps/accounts/serializers/account/template.py b/apps/accounts/serializers/account/template.py index 635c221bf..ea302c3fd 100644 --- a/apps/accounts/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -15,6 +15,9 @@ class PasswordRulesSerializer(serializers.Serializer): uppercase = serializers.BooleanField(default=True, label=_('Uppercase')) digit = serializers.BooleanField(default=True, label=_('Digit')) symbol = serializers.BooleanField(default=True, label=_('Special symbol')) + exclude_symbols = serializers.CharField( + default='', allow_blank=True, max_length=16, label=_('Exclude symbol') + ) class AccountTemplateSerializer(BaseAccountSerializer): diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py index 8a79c1aa4..a20eba291 100644 --- a/apps/accounts/tasks/__init__.py +++ b/apps/accounts/tasks/__init__.py @@ -2,5 +2,6 @@ from .automation import * from .backup_account import * from .gather_accounts import * from .push_account import * +from .remove_account import * from .template import * from .verify_account import * diff --git a/apps/accounts/tasks/remove_account.py b/apps/accounts/tasks/remove_account.py new file mode 100644 index 000000000..269a7f349 --- /dev/null +++ b/apps/accounts/tasks/remove_account.py @@ -0,0 +1,31 @@ +from celery import shared_task +from django.utils.translation import gettext_noop, gettext_lazy as _ + +from accounts.const import AutomationTypes +from accounts.tasks.common import quickstart_automation_by_snapshot +from common.utils import get_logger + +logger = get_logger(__file__) + +__all__ = ['remove_accounts_task'] + + +@shared_task( + queue="ansible", verbose_name=_('Remove account'), + activity_callback=lambda self, gather_account_ids, *args, **kwargs: (gather_account_ids, None) +) +def remove_accounts_task(gather_account_ids): + from accounts.models import GatheredAccount + + gather_accounts = GatheredAccount.objects.filter( + id__in=gather_account_ids + ) + task_name = gettext_noop("Remove account") + + task_snapshot = { + 'assets': [str(i.asset_id) for i in gather_accounts], + 'gather_accounts': [str(i.id) for i in gather_accounts], + } + + tp = AutomationTypes.remove_account + quickstart_automation_by_snapshot(task_name, tp, task_snapshot) diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py index 229cb0111..9df67daf6 100644 --- a/apps/accounts/utils.py +++ b/apps/accounts/utils.py @@ -30,7 +30,8 @@ class SecretGenerator: 'lower': rules['lowercase'], 'upper': rules['uppercase'], 'digit': rules['digit'], - 'special_char': rules['symbol'] + 'special_char': rules['symbol'], + 'exclude_chars': rules.get('exclude_symbols', ''), } return random_string(**rules) @@ -46,18 +47,10 @@ class SecretGenerator: def validate_password_for_ansible(password): """ 校验 Ansible 不支持的特殊字符 """ - # validate password contains left double curly bracket - # check password not contains `{{` - # Ansible 推送的时候不支持 - if '{{' in password or '}}' in password: - raise serializers.ValidationError(_('Password can not contains `{{` or `}}`')) - if '{%' in password or '%}' in password: - raise serializers.ValidationError(_('Password can not contains `{%` or `%}`')) - # Ansible Windows 推送的时候不支持 - # if "'" in password: - # raise serializers.ValidationError(_("Password can not contains `'` ")) - # if '"' in password: - # raise serializers.ValidationError(_('Password can not contains `"` ')) + if password.startswith('{{') and password.endswith('}}'): + raise serializers.ValidationError( + _('If the password starts with {{` and ends with }} `, then the password is not allowed.') + ) def validate_ssh_key(ssh_key, passphrase=None): diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index 2043f274d..8f7cc531a 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -11,7 +11,7 @@ __all__ = ['CommandFilterACLViewSet', 'CommandGroupViewSet'] class CommandGroupViewSet(OrgBulkModelViewSet): model = models.CommandGroup filterset_fields = ('name', 'command_filters') - search_fields = filterset_fields + search_fields = ('name',) serializer_class = serializers.CommandGroupSerializer diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 8ae0cd9bd..9eccd4977 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -2,7 +2,6 @@ from .asset import * from .category import * from .domain import * from .favorite_asset import * -from .label import * from .mixin import * from .node import * from .platform import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 4b36b4bbe..e0b1aa3bf 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -3,7 +3,6 @@ from collections import defaultdict import django_filters -from django.db.models import Q from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from rest_framework import status @@ -14,7 +13,7 @@ from rest_framework.status import HTTP_200_OK from accounts.tasks import push_accounts_to_assets_task, verify_accounts_connectivity_task from assets import serializers from assets.exceptions import NotSupportedTemporarilyError -from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend +from assets.filters import IpInFilterBackend, NodeFilterBackend from assets.models import Asset, Gateway, Platform, Protocol from assets.tasks import test_assets_connectivity_manual, update_assets_hardware_info_manual from common.api import SuggestionMixin @@ -33,7 +32,6 @@ __all__ = [ class AssetFilterSet(BaseFilterSet): - labels = django_filters.CharFilter(method='filter_labels') platform = django_filters.CharFilter(method='filter_platform') domain = django_filters.CharFilter(method='filter_domain') type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact") @@ -64,7 +62,7 @@ class AssetFilterSet(BaseFilterSet): class Meta: model = Asset fields = [ - "id", "name", "address", "is_active", "labels", + "id", "name", "address", "is_active", "type", "category", "platform", ] @@ -87,16 +85,6 @@ class AssetFilterSet(BaseFilterSet): value = value.split(',') return queryset.filter(protocols__name__in=value).distinct() - @staticmethod - def filter_labels(queryset, name, value): - if ':' in value: - n, v = value.split(':', 1) - queryset = queryset.filter(labels__name=n, labels__value=v) - else: - q = Q(labels__name__contains=value) | Q(labels__value__contains=value) - queryset = queryset.filter(q).distinct() - return queryset - class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): """ @@ -105,7 +93,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): model = Asset filterset_class = AssetFilterSet search_fields = ("name", "address", "comment") - ordering_fields = ('name', 'connectivity', 'platform', 'date_updated') + ordering_fields = ('name', 'connectivity', 'platform', 'date_updated', 'date_created') serializer_classes = ( ("default", serializers.AssetSerializer), ("platform", serializers.PlatformSerializer), @@ -121,7 +109,7 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ("sync_platform_protocols", "assets.change_asset"), ) extra_filter_backends = [ - LabelFilterBackend, IpInFilterBackend, + IpInFilterBackend, NodeFilterBackend, AttrRulesFilterBackend ] diff --git a/apps/assets/api/label.py b/apps/assets/api/label.py deleted file mode 100644 index d970d2180..000000000 --- a/apps/assets/api/label.py +++ /dev/null @@ -1,43 +0,0 @@ -# ~*~ coding: utf-8 ~*~ -# Copyright (C) 2014-2018 Beijing DuiZhan Technology Co.,Ltd. All Rights Reserved. -# -# Licensed under the GNU General Public License v2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.gnu.org/licenses/gpl-2.0.html -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from django.db.models import Count - -from common.utils import get_logger -from orgs.mixins.api import OrgBulkModelViewSet -from ..models import Label -from .. import serializers - - -logger = get_logger(__file__) -__all__ = ['LabelViewSet'] - - -class LabelViewSet(OrgBulkModelViewSet): - model = Label - filterset_fields = ("name", "value") - search_fields = filterset_fields - serializer_class = serializers.LabelSerializer - - def list(self, request, *args, **kwargs): - if request.query_params.get("distinct"): - self.serializer_class = serializers.LabelDistinctSerializer - self.queryset = self.queryset.values("name").distinct() - return super().list(request, *args, **kwargs) - - def get_queryset(self): - self.queryset = Label.objects.prefetch_related( - 'assets').annotate(asset_count=Count("assets")) - return self.queryset diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index 817952d46..2a74243d0 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -17,6 +17,62 @@ from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) +class SSHTunnelManager: + def __init__(self, *args, **kwargs): + self.gateway_servers = dict() + + @staticmethod + def file_to_json(path): + with open(path, 'r') as f: + d = json.load(f) + return d + + @staticmethod + def json_to_file(path, data): + with open(path, 'w') as f: + json.dump(data, f, indent=4, sort_keys=True) + + def local_gateway_prepare(self, runner): + info = self.file_to_json(runner.inventory) + servers, not_valid = [], [] + for k, host in info['all']['hosts'].items(): + jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway') + if not jms_gateway: + continue + try: + server = SSHTunnelForwarder( + (jms_gateway['address'], jms_gateway['port']), + ssh_username=jms_gateway['username'], + ssh_password=jms_gateway['secret'], + ssh_pkey=jms_gateway['private_key_path'], + remote_bind_address=(jms_asset['address'], jms_asset['port']) + ) + server.start() + except Exception as e: + err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '') + print(f'\033[31m {err_msg} 原因: {e} \033[0m\n') + not_valid.append(k) + else: + local_bind_port = server.local_bind_port + host['ansible_host'] = jms_asset['address'] = host['login_host'] = '127.0.0.1' + host['ansible_port'] = jms_asset['port'] = host['login_port'] = local_bind_port + servers.append(server) + + # 网域不可连接的,就不继续执行此资源的后续任务了 + for a in set(not_valid): + info['all']['hosts'].pop(a) + self.json_to_file(runner.inventory, info) + self.gateway_servers[runner.id] = servers + + def local_gateway_clean(self, runner): + servers = self.gateway_servers.get(runner.id, []) + for s in servers: + try: + s.stop() + except Exception: + pass + + class PlaybookCallback(DefaultCallback): def playbook_on_stats(self, event_data, **kwargs): super().playbook_on_stats(event_data, **kwargs) @@ -37,7 +93,6 @@ class BasePlaybookManager: # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 # 然后根据执行方式分组, 再根据 bulk_size 分组, 生成不同的 playbook self.playbooks = [] - self.gateway_servers = dict() params = self.execution.snapshot.get('params') self.params = params or {} @@ -157,22 +212,19 @@ class BasePlaybookManager: os.chmod(key_path, 0o400) return key_path - def generate_inventory(self, platformed_assets, inventory_path): + def generate_inventory(self, platformed_assets, inventory_path, protocol): inventory = JMSInventory( assets=platformed_assets, account_prefer=self.ansible_account_prefer, account_policy=self.ansible_account_policy, host_callback=self.host_callback, task_type=self.__class__.method_type(), + protocol=protocol, ) inventory.write_to_file(inventory_path) - def generate_playbook(self, platformed_assets, platform, sub_playbook_dir): - method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type())) - method = self.method_id_meta_mapper.get(method_id) - if not method: - logger.error("Method not found: {}".format(method_id)) - return + @staticmethod + def generate_playbook(method, sub_playbook_dir): method_playbook_dir_path = method['dir'] sub_playbook_path = os.path.join(sub_playbook_dir, 'project', 'main.yml') shutil.copytree(method_playbook_dir_path, os.path.dirname(sub_playbook_path)) @@ -204,8 +256,16 @@ class BasePlaybookManager: sub_dir = '{}_{}'.format(platform.name, i) playbook_dir = os.path.join(self.runtime_dir, sub_dir) inventory_path = os.path.join(self.runtime_dir, sub_dir, 'hosts.json') - self.generate_inventory(_assets, inventory_path) - playbook_path = self.generate_playbook(_assets, platform, playbook_dir) + + method_id = getattr(platform.automation, '{}_method'.format(self.__class__.method_type())) + method = self.method_id_meta_mapper.get(method_id) + + if not method: + logger.error("Method not found: {}".format(method_id)) + continue + protocol = method.get('protocol') + self.generate_inventory(_assets, inventory_path, protocol) + playbook_path = self.generate_playbook(method, playbook_dir) if not playbook_path: continue @@ -247,66 +307,10 @@ class BasePlaybookManager: def on_runner_failed(self, runner, e): print("Runner failed: {} {}".format(e, self)) - @staticmethod - def file_to_json(path): - with open(path, 'r') as f: - d = json.load(f) - return d - @staticmethod def json_dumps(data): return json.dumps(data, indent=4, sort_keys=True) - @staticmethod - def json_to_file(path, data): - with open(path, 'w') as f: - json.dump(data, f, indent=4, sort_keys=True) - - def local_gateway_prepare(self, runner): - info = self.file_to_json(runner.inventory) - servers, not_valid = [], [] - for k, host in info['all']['hosts'].items(): - jms_asset, jms_gateway = host.get('jms_asset'), host.get('gateway') - if not jms_gateway: - continue - try: - server = SSHTunnelForwarder( - (jms_gateway['address'], jms_gateway['port']), - ssh_username=jms_gateway['username'], - ssh_password=jms_gateway['secret'], - ssh_pkey=jms_gateway['private_key_path'], - remote_bind_address=(jms_asset['address'], jms_asset['port']) - ) - server.start() - except Exception as e: - err_msg = 'Gateway is not active: %s' % jms_asset.get('name', '') - print(f'\033[31m {err_msg} 原因: {e} \033[0m\n') - not_valid.append(k) - else: - host['ansible_host'] = jms_asset['address'] = '127.0.0.1' - host['ansible_port'] = jms_asset['port'] = server.local_bind_port - servers.append(server) - - # 网域不可连接的,就不继续执行此资源的后续任务了 - for a in set(not_valid): - info['all']['hosts'].pop(a) - self.json_to_file(runner.inventory, info) - self.gateway_servers[runner.id] = servers - - def local_gateway_clean(self, runner): - servers = self.gateway_servers.get(runner.id, []) - for s in servers: - try: - s.stop() - except Exception: - pass - - def before_runner_start(self, runner): - self.local_gateway_prepare(runner) - - def after_runner_end(self, runner): - self.local_gateway_clean(runner) - def delete_runtime_dir(self): if settings.DEBUG_DEV: return @@ -326,14 +330,15 @@ class BasePlaybookManager: for i, runner in enumerate(runners, start=1): if len(runners) > 1: print(">>> 开始执行第 {} 批任务".format(i)) - self.before_runner_start(runner) + ssh_tunnel = SSHTunnelManager() + ssh_tunnel.local_gateway_prepare(runner) try: cb = runner.run(**kwargs) self.on_runner_success(runner, cb) except Exception as e: self.on_runner_failed(runner, e) finally: - self.after_runner_end(runner) + ssh_tunnel.local_gateway_clean(runner) print('\n') self.execution.status = 'success' self.execution.date_finished = timezone.now() diff --git a/apps/assets/automations/gather_facts/database/mysql/main.yml b/apps/assets/automations/gather_facts/database/mysql/main.yml index b9d1fce8c..348a2150d 100644 --- a/apps/assets/automations/gather_facts/database/mysql/main.yml +++ b/apps/assets/automations/gather_facts/database/mysql/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_python_interpreter: /opt/py3/bin/python + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Get info @@ -10,10 +11,10 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version register: db_info diff --git a/apps/assets/automations/gather_facts/host/posix/manifest.yml b/apps/assets/automations/gather_facts/host/posix/manifest.yml index a92c496e9..d1a833311 100644 --- a/apps/assets/automations/gather_facts/host/posix/manifest.yml +++ b/apps/assets/automations/gather_facts/host/posix/manifest.yml @@ -7,6 +7,6 @@ type: method: gather_facts i18n: Gather posix facts: - zh: 使用 Ansible 指令 gather_facts 从主机获取设备信息 - en: Gather facts from asset using gather_facts - ja: gather_factsを使用してPosixから情報を収集する + zh: '使用 Ansible 指令 gather_facts 从主机获取设备信息' + en: 'Gather facts from asset using gather_facts' + ja: 'gather_factsを使用してPosixから情報を収集する' diff --git a/apps/assets/automations/gather_facts/host/windows/manifest.yml b/apps/assets/automations/gather_facts/host/windows/manifest.yml index 809208e10..7068f4d6e 100644 --- a/apps/assets/automations/gather_facts/host/windows/manifest.yml +++ b/apps/assets/automations/gather_facts/host/windows/manifest.yml @@ -7,6 +7,6 @@ type: - windows i18n: Gather facts windows: - zh: 使用 Ansible 指令 gather_facts 从 Windows 获取设备信息 - en: Gather facts from Windows using gather_facts - ja: gather_factsを使用してWindowsから情報を収集する + zh: '使用 Ansible 指令 gather_facts 从 Windows 获取设备信息' + en: 'Gather facts from Windows using gather_facts' + ja: 'gather_factsを使用してWindowsから情報を収集する' diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py index 8db474d7b..1453cc7a1 100644 --- a/apps/assets/automations/methods.py +++ b/apps/assets/automations/methods.py @@ -31,7 +31,7 @@ def generate_serializer(data): return create_serializer_class(serializer_name, params) -def get_platform_automation_methods(path): +def get_platform_automation_methods(path, lang=None): methods = [] for root, dirs, files in os.walk(path, topdown=False): for name in files: @@ -40,7 +40,7 @@ def get_platform_automation_methods(path): continue with open(path, 'r', encoding='utf8') as f: - manifest = yaml_load_with_i18n(f) + manifest = yaml_load_with_i18n(f, lang=lang) check_platform_method(manifest, path) manifest['dir'] = os.path.dirname(path) manifest['params_serializer'] = generate_serializer(manifest) diff --git a/apps/assets/automations/ping/custom/rdp/main.yml b/apps/assets/automations/ping/custom/rdp/main.yml index a68670998..75e40c027 100644 --- a/apps/assets/automations/ping/custom/rdp/main.yml +++ b/apps/assets/automations/ping/custom/rdp/main.yml @@ -10,6 +10,6 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'rdp') | map(attribute='port') | first }}" + login_port: "{{ jms_asset.port }}" login_secret_type: "{{ jms_account.secret_type }}" login_private_key_path: "{{ jms_account.private_key_path }}" diff --git a/apps/assets/automations/ping/custom/rdp/manifest.yml b/apps/assets/automations/ping/custom/rdp/manifest.yml index 77b8a855e..b8346c3f2 100644 --- a/apps/assets/automations/ping/custom/rdp/manifest.yml +++ b/apps/assets/automations/ping/custom/rdp/manifest.yml @@ -6,8 +6,10 @@ category: type: - windows method: ping +protocol: rdp + i18n: Ping by pyfreerdp: - zh: 使用 Python 模块 pyfreerdp 测试主机可连接性 - en: Ping by pyfreerdp module - ja: Pyfreerdpモジュールを使用してホストにPingする + zh: '使用 Python 模块 pyfreerdp 测试主机可连接性' + en: 'Ping by pyfreerdp module' + ja: 'Pyfreerdpモジュールを使用してホストにPingする' diff --git a/apps/assets/automations/ping/custom/ssh/main.yml b/apps/assets/automations/ping/custom/ssh/main.yml index 925d3f2e1..b974425be 100644 --- a/apps/assets/automations/ping/custom/ssh/main.yml +++ b/apps/assets/automations/ping/custom/ssh/main.yml @@ -11,7 +11,7 @@ login_user: "{{ jms_account.username }}" login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" - login_port: "{{ jms_asset.protocols | selectattr('name', 'equalto', 'ssh') | map(attribute='port') | first }}" + login_port: "{{ jms_asset.port }}" login_secret_type: "{{ jms_account.secret_type }}" login_private_key_path: "{{ jms_account.private_key_path }}" become: "{{ custom_become | default(False) }}" diff --git a/apps/assets/automations/ping/custom/ssh/manifest.yml b/apps/assets/automations/ping/custom/ssh/manifest.yml index d57a50a2e..7a7068108 100644 --- a/apps/assets/automations/ping/custom/ssh/manifest.yml +++ b/apps/assets/automations/ping/custom/ssh/manifest.yml @@ -6,8 +6,10 @@ category: type: - all method: ping +protocol: ssh + i18n: Ping by paramiko: - zh: 使用 Python 模块 paramiko 测试主机可连接性 - en: Ping by paramiko module - ja: Paramikoモジュールを使用してホストにPingする + zh: '使用 Python 模块 paramiko 测试主机可连接性' + en: 'Ping by paramiko module' + ja: 'Paramikoモジュールを使用してホストにPingする' diff --git a/apps/assets/automations/ping/database/mysql/main.yml b/apps/assets/automations/ping/database/mysql/main.yml index 2180610d4..f99333bdb 100644 --- a/apps/assets/automations/ping/database/mysql/main.yml +++ b/apps/assets/automations/ping/database/mysql/main.yml @@ -2,6 +2,7 @@ gather_facts: no vars: ansible_python_interpreter: /opt/py3/bin/python + check_ssl: "{{ jms_asset.spec_info.use_ssl and not jms_asset.spec_info.allow_invalid_cert }}" tasks: - name: Test MySQL connection @@ -10,8 +11,8 @@ login_password: "{{ jms_account.secret }}" login_host: "{{ jms_asset.address }}" login_port: "{{ jms_asset.port }}" - check_hostname: "{{ omit if not jms_asset.spec_info.use_ssl else jms_asset.spec_info.allow_invalid_cert }}" - ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) }}" - client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) }}" - client_key: "{{ jms_asset.secret_info.client_key | default(omit) }}" + check_hostname: "{{ check_ssl if check_ssl else omit }}" + ca_cert: "{{ jms_asset.secret_info.ca_cert | default(omit) if check_ssl else omit }}" + client_cert: "{{ jms_asset.secret_info.client_cert | default(omit) if check_ssl else omit }}" + client_key: "{{ jms_asset.secret_info.client_key | default(omit) if check_ssl else omit }}" filter: version diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 8654002b9..c33052c64 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -2,9 +2,11 @@ import json from collections import defaultdict from copy import deepcopy +from django.conf import settings from django.utils.translation import gettext as _ from common.db.models import ChoicesMixin +from jumpserver.utils import get_current_request from .category import Category from .cloud import CloudTypes from .custom import CustomTypes @@ -22,6 +24,8 @@ class AllTypes(ChoicesMixin): CloudTypes, WebTypes, CustomTypes, GPTTypes ] _category_constrains = {} + _automation_methods = None + _current_language = settings.LANGUAGE_CODE @classmethod def choices(cls): @@ -61,9 +65,28 @@ class AllTypes(ChoicesMixin): @classmethod def get_automation_methods(cls): - from assets.automations import platform_automation_methods as asset_methods - from accounts.automations import platform_automation_methods as account_methods - return asset_methods + account_methods + from assets.automations import methods as asset + from accounts.automations import methods as account + + automation_methods = \ + asset.platform_automation_methods + \ + account.platform_automation_methods + + request = get_current_request() + if request is None: + return automation_methods + + language = request.LANGUAGE_CODE + if cls._automation_methods is not None and language == cls._current_language: + automation_methods = cls._automation_methods + else: + automation_methods = \ + asset.get_platform_automation_methods(asset.BASE_DIR, language) + \ + account.get_platform_automation_methods(account.BASE_DIR, language) + + cls._current_language = language + cls._automation_methods = automation_methods + return cls._automation_methods @classmethod def set_automation_methods(cls, category, tp_name, constraints): diff --git a/apps/assets/filters.py b/apps/assets/filters.py index 17e8414bf..f1fe6d666 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -5,7 +5,6 @@ from rest_framework import filters from rest_framework.compat import coreapi, coreschema from assets.utils import get_node_from_request, is_query_node_all_assets -from .models import Label class AssetByNodeFilterBackend(filters.BaseFilterBackend): @@ -72,57 +71,6 @@ class NodeFilterBackend(filters.BaseFilterBackend): return queryset.filter(nodes__key=node.key).distinct() -class LabelFilterBackend(filters.BaseFilterBackend): - sep = ':' - query_arg = 'label' - - def get_schema_fields(self, view): - example = self.sep.join(['os', 'linux']) - return [ - coreapi.Field( - name=self.query_arg, location='query', required=False, - type='string', example=example, description='' - ) - ] - - def get_query_labels(self, request): - labels_query = request.query_params.getlist(self.query_arg) - if not labels_query: - return None - - q = None - for kv in labels_query: - if '#' in kv: - self.sep = '#' - break - - for kv in labels_query: - if self.sep not in kv: - continue - key, value = kv.strip().split(self.sep)[:2] - if not all([key, value]): - continue - if q: - q |= Q(name=key, value=value) - else: - q = Q(name=key, value=value) - if not q: - return [] - labels = Label.objects.filter(q, is_active=True) \ - .values_list('id', flat=True) - return labels - - def filter_queryset(self, request, queryset, view): - labels = self.get_query_labels(request) - if labels is None: - return queryset - if len(labels) == 0: - return queryset.none() - for label in labels: - queryset = queryset.filter(labels=label) - return queryset - - class IpInFilterBackend(filters.BaseFilterBackend): def filter_queryset(self, request, queryset, view): ips = request.query_params.get('ips') diff --git a/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py b/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py index 7cbf78a43..6392158fd 100644 --- a/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py +++ b/apps/assets/migrations/0002_auto_20180105_1807_squashed_0009_auto_20180307_1212.py @@ -123,7 +123,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='asset', name='nodes', - field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), + field=models.ManyToManyField(default=assets.models.asset.default_node, related_name='assets', to='assets.Node', verbose_name='Node'), ), migrations.AddField( model_name='systemuser', diff --git a/apps/assets/migrations/0007_auto_20180225_1815.py b/apps/assets/migrations/0007_auto_20180225_1815.py index 4ce2b1e05..1097dd182 100644 --- a/apps/assets/migrations/0007_auto_20180225_1815.py +++ b/apps/assets/migrations/0007_auto_20180225_1815.py @@ -50,7 +50,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='asset', name='nodes', - field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Nodes'), + field=models.ManyToManyField(default=assets.models.default_node, related_name='assets', to='assets.Node', verbose_name='Node'), ), migrations.AddField( model_name='systemuser', diff --git a/apps/assets/migrations/0107_automation.py b/apps/assets/migrations/0107_automation.py index 56c2cf4eb..6841cb912 100644 --- a/apps/assets/migrations/0107_automation.py +++ b/apps/assets/migrations/0107_automation.py @@ -31,7 +31,7 @@ class Migration(migrations.Migration): ('type', models.CharField(max_length=16, verbose_name='Type')), ('is_active', models.BooleanField(default=True, verbose_name='Is active')), ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), - ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), + ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Node')), ], options={ 'verbose_name': 'Automation task', diff --git a/apps/assets/migrations/0126_remove_asset_labels.py b/apps/assets/migrations/0126_remove_asset_labels.py new file mode 100644 index 000000000..44590dc4c --- /dev/null +++ b/apps/assets/migrations/0126_remove_asset_labels.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-11-22 07:33 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0125_auto_20231011_1053'), + ('labels', '0002_auto_20231103_1659'), + ] + + operations = [ + migrations.RemoveField( + model_name='asset', + name='labels', + ), + ] diff --git a/apps/assets/migrations/0127_automation_remove_account.py b/apps/assets/migrations/0127_automation_remove_account.py new file mode 100644 index 000000000..e0746326f --- /dev/null +++ b/apps/assets/migrations/0127_automation_remove_account.py @@ -0,0 +1,55 @@ +# Generated by Django 4.1.10 on 2023-12-05 10:03 +from functools import reduce + +from django.db import migrations, models +from django.db.models import F + + +def migrate_automation_ansible_remove_account(apps, *args): + automation_model = apps.get_model('assets', 'PlatformAutomation') + automation_map = { + ('oracle',): 'remove_account_oracle', + ('windows',): 'remove_account_windows', + ('mongodb',): 'remove_account_mongodb', + ('linux', 'unix'): 'remove_account_posix', + ('sqlserver',): 'remove_account_sqlserver', + ('mysql', 'mariadb'): 'remove_account_mysql', + ('postgresql',): 'remove_account_postgresql', + } + + update_objs = [] + types = list(reduce(lambda x, y: x + y, automation_map.keys())) + qs = automation_model.objects.filter(platform__type__in=types).annotate(tp=F('platform__type')) + for automation in qs: + for types, method in automation_map.items(): + if automation.tp in types: + automation.remove_account_enabled = True + automation.remove_account_method = method + break + update_objs.append(automation) + automation_model.objects.bulk_update(update_objs, ['remove_account_enabled', 'remove_account_method']) + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0126_remove_asset_labels'), + ] + + operations = [ + migrations.AddField( + model_name='platformautomation', + name='remove_account_enabled', + field=models.BooleanField(default=False, verbose_name='Remove account enabled'), + ), + migrations.AddField( + model_name='platformautomation', + name='remove_account_method', + field=models.TextField(blank=True, max_length=32, null=True, verbose_name='Remove account method'), + ), + migrations.AddField( + model_name='platformautomation', + name='remove_account_params', + field=models.JSONField(default=dict, verbose_name='Remove account params'), + ), + migrations.RunPython(migrate_automation_ansible_remove_account) + ] diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index a22e552e9..558164df6 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -13,7 +13,9 @@ from django.utils.translation import gettext_lazy as _ from assets import const from common.db.fields import EncryptMixin from common.utils import lazyproperty +from labels.mixins import LabeledMixin from orgs.mixins.models import OrgManager, JMSOrgBaseModel +from rbac.models import ContentType from ..base import AbsConnectivity from ..platform import Platform @@ -150,7 +152,7 @@ class JSONFilterMixin: return None -class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseModel): +class Asset(NodesRelationMixin, LabeledMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseModel): Category = const.Category Type = const.AllTypes @@ -160,9 +162,8 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode domain = models.ForeignKey("assets.Domain", null=True, blank=True, related_name='assets', verbose_name=_("Domain"), on_delete=models.SET_NULL) nodes = models.ManyToManyField('assets.Node', default=default_node, related_name='assets', - verbose_name=_("Nodes")) + verbose_name=_("Node")) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) - labels = models.ManyToManyField('assets.Label', blank=True, related_name='assets', verbose_name=_("Labels")) gathered_info = models.JSONField(verbose_name=_('Gathered info'), default=dict, blank=True) # 资产的一些信息,如 硬件信息 custom_info = models.JSONField(verbose_name=_('Custom info'), default=dict) @@ -171,6 +172,13 @@ class Asset(NodesRelationMixin, AbsConnectivity, JSONFilterMixin, JMSOrgBaseMode def __str__(self): return '{0.name}({0.address})'.format(self) + def get_labels(self): + from labels.models import Label, LabeledResource + res_type = ContentType.objects.get_for_model(self.__class__) + label_ids = LabeledResource.objects.filter(res_type=res_type, res_id=self.id) \ + .values_list('label_id', flat=True) + return Label.objects.filter(id__in=label_ids) + @staticmethod def get_spec_values(instance, fields): info = {} diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 27e5ed74a..c3e1fa639 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -15,7 +15,7 @@ from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): accounts = models.JSONField(default=list, verbose_name=_("Accounts")) - nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) + nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Node")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) type = models.CharField(max_length=16, verbose_name=_('Type')) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index ec6f1be3e..63fd7ca34 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -29,7 +29,7 @@ class CommandFilter(OrgModelMixin): ) nodes = models.ManyToManyField( 'assets.Node', related_name='cmd_filters', blank=True, - verbose_name=_("Nodes") + verbose_name=_("Node") ) assets = models.ManyToManyField( 'assets.Asset', related_name='cmd_filters', blank=True, diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 587f41d7c..e424a2d46 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -6,6 +6,7 @@ from django.db import models from django.utils.translation import gettext_lazy as _ from common.utils import get_logger +from labels.mixins import LabeledMixin from orgs.mixins.models import JMSOrgBaseModel from .gateway import Gateway @@ -14,7 +15,7 @@ logger = get_logger(__file__) __all__ = ['Domain'] -class Domain(JMSOrgBaseModel): +class Domain(LabeledMixin, JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) class Meta: diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index 8fed01acf..a93a96863 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -9,6 +9,7 @@ from common.db.models import JMSBaseModel __all__ = ['Platform', 'PlatformProtocol', 'PlatformAutomation'] from common.utils import lazyproperty +from labels.mixins import LabeledMixin class PlatformProtocol(models.Model): @@ -71,10 +72,16 @@ class PlatformAutomation(models.Model): max_length=32, blank=True, null=True, verbose_name=_("Gather facts method") ) gather_accounts_params = models.JSONField(default=dict, verbose_name=_("Gather facts params")) + + remove_account_enabled = models.BooleanField(default=False, verbose_name=_("Remove account enabled")) + remove_account_method = models.TextField( + max_length=32, blank=True, null=True, verbose_name=_("Remove account method") + ) + remove_account_params = models.JSONField(default=dict, verbose_name=_("Remove account params")) platform = models.OneToOneField('Platform', on_delete=models.CASCADE, related_name='automation', null=True) -class Platform(JMSBaseModel): +class Platform(LabeledMixin, JMSBaseModel): """ 对资产提供 约束和默认值 对资产进行抽象 diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index cbf21454d..e071e24c0 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -2,11 +2,10 @@ # from .asset import * -from .label import * -from .node import * -from .gateway import * +from .automations import * +from .cagegory import * from .domain import * from .favorite_asset import * +from .gateway import * +from .node import * from .platform import * -from .cagegory import * -from .automations import * diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index 75d8c4c19..6c45944de 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -11,13 +11,14 @@ from accounts.serializers import AccountSerializer from common.const import UUID_PATTERN from common.serializers import ( WritableNestedModelSerializer, SecretReadableMixin, - CommonModelSerializer, MethodSerializer + CommonModelSerializer, MethodSerializer, ResourceLabelsMixin ) from common.serializers.common import DictSerializer from common.serializers.fields import LabeledChoiceField +from labels.models import Label from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ...const import Category, AllTypes -from ...models import Asset, Node, Platform, Label, Protocol +from ...models import Asset, Node, Platform, Protocol __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', @@ -117,10 +118,9 @@ class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer): } -class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): +class AssetSerializer(BulkOrgResourceModelSerializer, ResourceLabelsMixin, WritableNestedModelSerializer): category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) - labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols'), default=()) accounts = AssetAccountSerializer(many=True, required=False, allow_null=True, write_only=True, label=_('Account')) nodes_display = serializers.ListField(read_only=False, required=False, label=_("Node path")) @@ -201,8 +201,9 @@ class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSeriali @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('domain', 'nodes', 'labels', 'protocols') \ + 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")) return queryset diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index a22d1a9ad..d2b3e3550 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from common.serializers import ResourceLabelsMixin from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .gateway import GatewayWithAccountSecretSerializer @@ -11,7 +12,7 @@ from ..models import Domain, Asset __all__ = ['DomainSerializer', 'DomainWithGatewaySerializer'] -class DomainSerializer(BulkOrgResourceModelSerializer): +class DomainSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer): gateways = ObjectRelatedField( many=True, required=False, label=_('Gateway'), read_only=True, ) @@ -41,6 +42,12 @@ class DomainSerializer(BulkOrgResourceModelSerializer): instance = super().update(instance, validated_data) return instance + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset \ + .prefetch_related('labels', 'labels__label') + return queryset + class DomainWithGatewaySerializer(serializers.ModelSerializer): gateways = GatewayWithAccountSecretSerializer(many=True, read_only=True) diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py deleted file mode 100644 index 3d913aeea..000000000 --- a/apps/assets/serializers/label.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -# -from django.db.models import Count -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - -from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from ..models import Label - - -class LabelSerializer(BulkOrgResourceModelSerializer): - asset_count = serializers.ReadOnlyField(label=_("Assets amount")) - - class Meta: - model = Label - fields_mini = ['id', 'name'] - fields_small = fields_mini + [ - 'value', 'category', 'is_active', - 'date_created', 'comment', - ] - fields_m2m = ['asset_count', 'assets'] - fields = fields_small + fields_m2m - read_only_fields = ( - 'category', 'date_created', 'asset_count', - ) - extra_kwargs = { - 'assets': {'required': False, 'label': _('Asset')} - } - - @classmethod - def setup_eager_loading(cls, queryset): - queryset = queryset.prefetch_related('assets') \ - .annotate(asset_count=Count('assets')) - return queryset - - -class LabelDistinctSerializer(BulkOrgResourceModelSerializer): - value = serializers.SerializerMethodField() - - class Meta: - model = Label - fields = ("name", "value") - - @staticmethod - def get_value(obj): - labels = Label.objects.filter(name=obj["name"]) - return ', '.join([label.value for label in labels]) diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index 2a0634a43..f67df9906 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -5,7 +5,7 @@ from rest_framework.validators import UniqueValidator from common.serializers import ( WritableNestedModelSerializer, type_field_map, MethodSerializer, - DictSerializer, create_serializer_class + DictSerializer, create_serializer_class, ResourceLabelsMixin ) from common.serializers.fields import LabeledChoiceField from common.utils import lazyproperty @@ -123,7 +123,7 @@ class PlatformCustomField(serializers.Serializer): choices = serializers.ListField(default=list, label=_("Choices"), required=False) -class PlatformSerializer(WritableNestedModelSerializer): +class PlatformSerializer(ResourceLabelsMixin, WritableNestedModelSerializer): SU_METHOD_CHOICES = [ ("sudo", "sudo su -"), ("su", "su - "), @@ -160,6 +160,7 @@ class PlatformSerializer(WritableNestedModelSerializer): fields = fields_small + [ "protocols", "domain_enabled", "su_enabled", "su_method", "automation", "comment", "custom_fields", + "labels" ] + read_only_fields extra_kwargs = { "su_enabled": {"label": _('Su enabled')}, @@ -201,9 +202,8 @@ class PlatformSerializer(WritableNestedModelSerializer): @classmethod def setup_eager_loading(cls, queryset): - queryset = queryset.prefetch_related( - 'protocols', 'automation' - ) + queryset = queryset.prefetch_related('protocols', 'automation') \ + .prefetch_related('labels', 'labels__label') return queryset def validate_protocols(self, protocols): diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 5644d7dc0..408550ece 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -17,7 +17,6 @@ router.register(r'clouds', api.CloudViewSet, 'cloud') router.register(r'gpts', api.GPTViewSet, 'gpt') router.register(r'customs', api.CustomViewSet, 'custom') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') -router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py index 870c25c9c..e8fe43c22 100644 --- a/apps/audits/backends/db.py +++ b/apps/audits/backends/db.py @@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _ from audits.models import OperateLog +from perms.const import ActionChoices class OperateLogStore(object): @@ -45,20 +46,29 @@ class OperateLogStore(object): before[k], after[k] = before_value, after_value return before, after + @staticmethod + def _get_special_handler(resource_type): + # 根据资源类型,处理特殊字段 + resource_map = { + 'Asset permission': lambda k, v: ActionChoices.display(int(v)) if k == 'Actions' else v + } + return resource_map.get(resource_type, lambda k, v: v) + @classmethod - def convert_diff_friendly(cls, raw_diff): + def convert_diff_friendly(cls, op_log): diff_list = list() - for k, v in raw_diff.items(): + handler = cls._get_special_handler(op_log.resource_type) + for k, v in op_log.diff.items(): before, after = v.split(cls.SEP, 1) diff_list.append({ 'field': _(k), - 'before': before if before else _('empty'), - 'after': after if after else _('empty'), + 'before': handler(k, before) if before else _('empty'), + 'after': handler(k, after) if after else _('empty'), }) return diff_list def save(self, **kwargs): - log_id = kwargs.pop('id', None) + log_id = kwargs.get('id', None) before = kwargs.pop('before') or {} after = kwargs.pop('after') or {} diff --git a/apps/audits/migrations/0024_usersession.py b/apps/audits/migrations/0024_usersession.py index 3cca28f75..c5a6c9331 100644 --- a/apps/audits/migrations/0024_usersession.py +++ b/apps/audits/migrations/0024_usersession.py @@ -1,9 +1,10 @@ # Generated by Django 4.1.10 on 2023-09-15 08:58 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): @@ -31,7 +32,7 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'User session', 'ordering': ['-date_created'], - 'permissions': [('offline_usersession', 'Offline ussr session')], + 'permissions': [('offline_usersession', 'Offline user session')], }, ), ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 97496102b..64a0ebc5b 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -305,5 +305,5 @@ class UserSession(models.Model): ordering = ['-date_created'] verbose_name = _('User session') permissions = [ - ('offline_usersession', _('Offline ussr session')), + ('offline_usersession', _('Offline user session')), ] diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index b68d1a516..78c42fdd0 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -77,10 +77,7 @@ class OperateLogActionDetailSerializer(serializers.ModelSerializer): fields = ('diff',) def to_representation(self, instance): - data = super().to_representation(instance) - diff = OperateLogStore.convert_diff_friendly(data['diff']) - data['diff'] = diff - return data + return {'diff': OperateLogStore.convert_diff_friendly(instance)} class OperateLogSerializer(BulkOrgResourceModelSerializer): diff --git a/apps/audits/signal_handlers/login_log.py b/apps/audits/signal_handlers/login_log.py index 7765e470b..5829e4f5e 100644 --- a/apps/audits/signal_handlers/login_log.py +++ b/apps/audits/signal_handlers/login_log.py @@ -36,6 +36,7 @@ class AuthBackendLabelMapping(LazyObject): backend_label_mapping[settings.AUTH_BACKEND_AUTH_TOKEN] = _("Auth Token") backend_label_mapping[settings.AUTH_BACKEND_WECOM] = _("WeCom") backend_label_mapping[settings.AUTH_BACKEND_FEISHU] = _("FeiShu") + backend_label_mapping[settings.AUTH_BACKEND_SLACK] = _("Slack") backend_label_mapping[settings.AUTH_BACKEND_DINGTALK] = _("DingTalk") backend_label_mapping[settings.AUTH_BACKEND_TEMP_TOKEN] = _("Temporary token") backend_label_mapping[settings.AUTH_BACKEND_PASSKEY] = _("Passkey") diff --git a/apps/audits/signal_handlers/operate_log.py b/apps/audits/signal_handlers/operate_log.py index 7d89f77bd..d095a7ad6 100644 --- a/apps/audits/signal_handlers/operate_log.py +++ b/apps/audits/signal_handlers/operate_log.py @@ -33,7 +33,9 @@ def on_m2m_changed(sender, action, instance, reverse, model, pk_set, **kwargs): with translation.override('en'): resource_type = instance._meta.verbose_name - current_instance = model_to_dict(instance, include_model_fields=False) + current_instance = model_to_dict( + instance, include_model_fields=False, include_related_fields=[model] + ) instance_id = current_instance.get('id') log_id, before_instance = get_instance_dict_from_cache(instance_id) @@ -176,7 +178,7 @@ def on_django_start_set_operate_log_monitor_models(sender, **kwargs): 'PermedAsset', 'PermedAccount', 'MenuPermission', 'Permission', 'TicketSession', 'ApplyLoginTicket', 'ApplyCommandTicket', 'ApplyLoginAssetTicket', - 'FavoriteAsset', + 'FavoriteAsset', 'Asset' } for i, app in enumerate(apps.get_models(), 1): app_name = app._meta.app_label diff --git a/apps/audits/utils.py b/apps/audits/utils.py index 44e858098..4312962e6 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -2,6 +2,7 @@ import copy from datetime import datetime from itertools import chain +from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation from django.db import models from common.db.fields import RelatedManager @@ -36,6 +37,9 @@ def _get_instance_field_value( if not include_model_fields and not getattr(f, 'primary_key', False): continue + if isinstance(f, GenericForeignKey): + continue + if isinstance(f, (models.FileField, models.ImageField)): continue @@ -65,37 +69,49 @@ def _get_instance_field_value( continue data.setdefault(k, v) continue - data.setdefault(str(f.verbose_name), value) + elif isinstance(f, GenericRelation): + value = [str(v) for v in value.all()] + elif isinstance(f, GenericForeignKey): + continue + try: + data.setdefault(str(f.verbose_name), value) + except Exception as e: + print(f.__dict__) + raise e return data def model_to_dict_for_operate_log( - instance, include_model_fields=True, include_related_fields=False + instance, include_model_fields=True, include_related_fields=None ): - model_need_continue_fields = ['date_updated'] - m2m_need_continue_fields = ['history_passwords'] + def get_related_values(f): + value = [] + if instance.pk is not None: + related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '') + if not related_name or related_name in ['history_passwords']: + return + try: + value = [str(i) for i in getattr(instance, related_name).all()] + except: + pass + if not value: + return + try: + field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name + data.setdefault(str(field_key), value) + except: + pass data = _get_instance_field_value( - instance, include_model_fields, model_need_continue_fields + instance, include_model_fields, ['date_updated'] ) if include_related_fields: opts = instance._meta - for f in opts.many_to_many: - value = [] - if instance.pk is not None: - related_name = getattr(f, 'attname', '') or getattr(f, 'related_name', '') - if not related_name or related_name in m2m_need_continue_fields: - continue - try: - value = [str(i) for i in getattr(instance, related_name).all()] - except: - pass - if not value: + for f in chain(opts.many_to_many, opts.related_objects): + related_model = getattr(f, 'related_model', None) + if related_model not in include_related_fields: continue - try: - field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name - data.setdefault(str(field_key), value) - except: - pass + get_related_values(f) + return data diff --git a/apps/authentication/api/__init__.py b/apps/authentication/api/__init__.py index 17e83813c..7a63c007c 100644 --- a/apps/authentication/api/__init__.py +++ b/apps/authentication/api/__init__.py @@ -4,7 +4,6 @@ from .access_key import * from .confirm import * from .connection_token import * -from .dingtalk import * from .feishu import * from .login_confirm import * from .mfa import * @@ -12,4 +11,4 @@ from .password import * from .sso import * from .temp_token import * from .token import * -from .wecom import * +from .common import * diff --git a/apps/authentication/api/common.py b/apps/authentication/api/common.py new file mode 100644 index 000000000..6624078a7 --- /dev/null +++ b/apps/authentication/api/common.py @@ -0,0 +1,51 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView + +from authentication import errors +from authentication.const import ConfirmType +from authentication.permissions import UserConfirmation +from common.api import RoleUserMixin, RoleAdminMixin +from common.exceptions import JMSException +from common.permissions import IsValidUser, OnlySuperUser +from common.utils import get_logger +from users.models import User + + +logger = get_logger(__file__) + + +class QRUnBindBase(APIView): + user: User + + def post(self, request: Request, backend: str, **kwargs): + backend_map = { + 'wecom': {'user_field': 'wecom_id', 'not_bind_err': errors.WeComNotBound}, + 'dingtalk': {'user_field': 'dingtalk_id', 'not_bind_err': errors.DingTalkNotBound}, + 'feishu': {'user_field': 'feishu_id', 'not_bind_err': errors.FeiShuNotBound}, + 'slack': {'user_field': 'slack_id', 'not_bind_err': errors.SlackNotBound}, + } + user = self.user + + backend_info = backend_map.get(backend) + if not backend_info: + raise JMSException( + _('The value in the parameter must contain %s') % ', '.join(backend_map.keys()) + ) + + if not getattr(user, backend_info['user_field'], None): + raise backend_info['not_bind_err'] + + setattr(user, backend_info['user_field'], None) + user.save() + return Response() + + +class QRUnBindForUserApi(RoleUserMixin, QRUnBindBase): + permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) + + +class QRUnBindForAdminApi(RoleAdminMixin, QRUnBindBase): + permission_classes = (OnlySuperUser,) + user_id_url_kwarg = 'user_id' diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 954fb6074..076767946 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -27,13 +27,13 @@ from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil from terminal.models import EndpointRule, Endpoint from users.const import FileNameConflictResolution -from users.const import RDPSmartSize +from users.const import RDPSmartSize, RDPColorQuality from users.models import Preference from ..models import ConnectionToken, date_expired_default from ..serializers import ( ConnectionTokenSerializer, ConnectionTokenSecretSerializer, SuperConnectionTokenSerializer, ConnectTokenAppletOptionSerializer, - ConnectionTokenReusableSerializer, + ConnectionTokenReusableSerializer, ConnectTokenVirtualAppOptionSerializer ) __all__ = ['ConnectionTokenViewSet', 'SuperConnectionTokenViewSet'] @@ -49,7 +49,6 @@ class RDPFileClientProtocolURLMixin: 'full address:s': '', 'username:s': '', 'use multimon:i': '0', - 'session bpp:i': '32', 'audiomode:i': '0', 'disable wallpaper:i': '0', 'disable full window drag:i': '0', @@ -100,10 +99,13 @@ class RDPFileClientProtocolURLMixin: rdp_options['winposstr:s'] = f'0,1,0,0,{width},{height}' rdp_options['dynamic resolution:i'] = '0' + color_quality = self.request.query_params.get('rdp_color_quality') + color_quality = color_quality if color_quality else os.getenv('JUMPSERVER_COLOR_DEPTH', RDPColorQuality.HIGH) + # 设置其他选项 - rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE) - rdp_options['session bpp:i'] = os.getenv('JUMPSERVER_COLOR_DEPTH', '32') + rdp_options['session bpp:i'] = color_quality rdp_options['audiomode:i'] = self.parse_env_bool('JUMPSERVER_DISABLE_AUDIO', 'false', '2', '0') + rdp_options['smart sizing:i'] = self.request.query_params.get('rdp_smart_size', RDPSmartSize.DISABLE) # 设置远程应用, 不是 Mstsc if token.connect_method != NativeClient.mstsc: @@ -464,6 +466,7 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): 'get_secret_detail': 'authentication.view_superconnectiontokensecret', 'get_applet_info': 'authentication.view_superconnectiontoken', 'release_applet_account': 'authentication.view_superconnectiontoken', + 'get_virtual_app_info': 'authentication.view_superconnectiontoken', } def get_queryset(self): @@ -529,14 +532,24 @@ class SuperConnectionTokenViewSet(ConnectionTokenViewSet): serializer = ConnectTokenAppletOptionSerializer(data) return Response(serializer.data) + @action(methods=['POST'], detail=False, url_path='virtual-app-option') + def get_virtual_app_info(self, *args, **kwargs): + token_id = self.request.data.get('id') + token = get_object_or_404(ConnectionToken, pk=token_id) + if token.is_expired: + return Response({'error': 'Token expired'}, status=status.HTTP_400_BAD_REQUEST) + data = token.get_virtual_app_option() + serializer = ConnectTokenVirtualAppOptionSerializer(data) + return Response(serializer.data) + @action(methods=['DELETE', 'POST'], detail=False, url_path='applet-account/release') def release_applet_account(self, *args, **kwargs): - account_id = self.request.data.get('id') - released = ConnectionToken.release_applet_account(account_id) + lock_key = self.request.data.get('id') + released = ConnectionToken.release_applet_account(lock_key) if released: - logger.debug('Release applet account success: {}'.format(account_id)) + logger.debug('Release applet account success: {}'.format(lock_key)) return Response({'msg': 'released'}) else: - logger.error('Release applet account error: {}'.format(account_id)) + logger.error('Release applet account error: {}'.format(lock_key)) return Response({'error': 'not found or expired'}, status=400) diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py deleted file mode 100644 index ad3bd26b1..000000000 --- a/apps/authentication/api/dingtalk.py +++ /dev/null @@ -1,35 +0,0 @@ -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.views import APIView - -from authentication import errors -from authentication.const import ConfirmType -from authentication.permissions import UserConfirmation -from common.api import RoleUserMixin, RoleAdminMixin -from common.permissions import IsValidUser -from common.utils import get_logger -from users.models import User - -logger = get_logger(__file__) - - -class DingTalkQRUnBindBase(APIView): - user: User - - def post(self, request: Request, **kwargs): - user = self.user - - if not user.dingtalk_id: - raise errors.DingTalkNotBound - - user.dingtalk_id = None - user.save() - return Response() - - -class DingTalkQRUnBindForUserApi(RoleUserMixin, DingTalkQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) - - -class DingTalkQRUnBindForAdminApi(RoleAdminMixin, DingTalkQRUnBindBase): - user_id_url_kwarg = 'user_id' diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index cf95bb6ea..ca90be807 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -2,39 +2,13 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView -from authentication import errors -from authentication.const import ConfirmType -from authentication.permissions import UserConfirmation -from common.api import RoleUserMixin, RoleAdminMixin from common.permissions import IsValidUser from common.utils import get_logger -from users.models import User + logger = get_logger(__name__) -class FeiShuQRUnBindBase(APIView): - user: User - - def post(self, request: Request, **kwargs): - user = self.user - - if not user.feishu_id: - raise errors.FeiShuNotBound - - user.feishu_id = None - user.save() - return Response() - - -class FeiShuQRUnBindForUserApi(RoleUserMixin, FeiShuQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) - - -class FeiShuQRUnBindForAdminApi(RoleAdminMixin, FeiShuQRUnBindBase): - user_id_url_kwarg = 'user_id' - - class FeiShuEventSubscriptionCallback(APIView): """ # https://open.feishu.cn/document/ukTMukTMukTM/uUTNz4SN1MjL1UzM diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py index 0756bad4e..6e48bda41 100644 --- a/apps/authentication/api/sso.py +++ b/apps/authentication/api/sso.py @@ -14,7 +14,7 @@ from common.api import JMSGenericViewSet from common.const.http import POST, GET from common.permissions import OnlySuperUser from common.serializers import EmptySerializer -from common.utils import reverse +from common.utils import reverse, safe_next_url from common.utils.timezone import utc_now from users.models import User from ..errors import SSOAuthClosed @@ -45,6 +45,7 @@ class SSOViewSet(AuthMixin, JMSGenericViewSet): username = serializer.validated_data['username'] user = User.objects.get(username=username) next_url = serializer.validated_data.get(NEXT_URL) + next_url = safe_next_url(next_url, request=request) operator = request.user.username # TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理 diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py deleted file mode 100644 index 6dcfe539c..000000000 --- a/apps/authentication/api/wecom.py +++ /dev/null @@ -1,35 +0,0 @@ -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.views import APIView - -from authentication import errors -from authentication.const import ConfirmType -from authentication.permissions import UserConfirmation -from common.api import RoleUserMixin, RoleAdminMixin -from common.permissions import IsValidUser -from common.utils import get_logger -from users.models import User - -logger = get_logger(__file__) - - -class WeComQRUnBindBase(APIView): - user: User - - def post(self, request: Request, **kwargs): - user = self.user - - if not user.wecom_id: - raise errors.WeComNotBound - - user.wecom_id = None - user.save() - return Response() - - -class WeComQRUnBindForUserApi(RoleUserMixin, WeComQRUnBindBase): - permission_classes = (IsValidUser, UserConfirmation.require(ConfirmType.RELOGIN),) - - -class WeComQRUnBindForAdminApi(RoleAdminMixin, WeComQRUnBindBase): - user_id_url_kwarg = 'user_id' diff --git a/apps/authentication/backends/drf.py b/apps/authentication/backends/drf.py index f6818e4a4..86999037d 100644 --- a/apps/authentication/backends/drf.py +++ b/apps/authentication/backends/drf.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext as _ from rest_framework import authentication, exceptions from common.auth import signature -from common.decorators import delay_run +from common.decorators import merge_delay_run from common.utils import get_object_or_none, get_request_ip_or_data, contains_ip from ..models import AccessKey, PrivateToken @@ -17,22 +17,24 @@ def date_more_than(d, seconds): return d is None or (timezone.now() - d).seconds > seconds -@delay_run(ttl=60) -def update_token_last_used(token): - token.date_last_used = timezone.now() - token.save(update_fields=['date_last_used']) +@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']) -@delay_run(ttl=60) -def update_user_last_used(user): - user.date_api_key_last_used = timezone.now() - user.save(update_fields=['date_api_key_last_used']) +@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']) def after_authenticate_update_date(user, token=None): - update_user_last_used(user) + update_user_last_used(users=(user,)) if token: - update_token_last_used(token) + update_token_last_used(tokens=(token,)) class AccessTokenAuthentication(authentication.BaseAuthentication): diff --git a/apps/authentication/backends/oidc/views.py b/apps/authentication/backends/oidc/views.py index 98bd2ef2a..56c1e4fb0 100644 --- a/apps/authentication/backends/oidc/views.py +++ b/apps/authentication/backends/oidc/views.py @@ -20,10 +20,11 @@ from django.core.exceptions import SuspiciousOperation from django.http import HttpResponseRedirect, QueryDict from django.urls import reverse from django.utils.crypto import get_random_string -from django.utils.http import url_has_allowed_host_and_scheme, urlencode +from django.utils.http import urlencode from django.views.generic import View from authentication.utils import build_absolute_uri_for_oidc +from common.utils import safe_next_url from .utils import get_logger logger = get_logger(__file__) @@ -100,8 +101,7 @@ class OIDCAuthRequestView(View): # Stores the "next" URL in the session if applicable. logger.debug(log_prompt.format('Stores next url in the session')) next_url = request.GET.get('next') - request.session['oidc_auth_next_url'] = next_url \ - if url_has_allowed_host_and_scheme(url=next_url, allowed_hosts=(request.get_host(),)) else None + request.session['oidc_auth_next_url'] = safe_next_url(next_url, request=request) # Redirects the user to authorization endpoint. logger.debug(log_prompt.format('Construct redirect url')) diff --git a/apps/authentication/backends/sso.py b/apps/authentication/backends/sso.py index 7bee484dc..66f15a3a4 100644 --- a/apps/authentication/backends/sso.py +++ b/apps/authentication/backends/sso.py @@ -55,6 +55,19 @@ class FeiShuAuthentication(JMSModelBackend): pass +class SlackAuthentication(JMSModelBackend): + """ + 什么也不做呀😺 + """ + + @staticmethod + def is_enabled(): + return settings.AUTH_SLACK + + def authenticate(self, request, **kwargs): + pass + + class AuthorizationTokenAuthentication(JMSModelBackend): """ 什么也不做呀😺 diff --git a/apps/authentication/errors/mfa.py b/apps/authentication/errors/mfa.py index 8a0844145..b1ace594d 100644 --- a/apps/authentication/errors/mfa.py +++ b/apps/authentication/errors/mfa.py @@ -33,6 +33,11 @@ class FeiShuNotBound(JMSException): default_detail = _('FeiShu is not bound') +class SlackNotBound(JMSException): + default_code = 'slack_not_bound' + default_detail = _('Slack is not bound') + + class PasswordInvalid(JMSException): default_code = 'passwd_invalid' default_detail = _('Your password is invalid') diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index 48c8e8e16..2ef28eab1 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -18,7 +18,7 @@ from common.utils import lazyproperty, pretty_string, bulk_get from common.utils.timezone import as_current_tz from orgs.mixins.models import JMSOrgBaseModel from orgs.utils import tmp_to_org -from terminal.models import Applet +from terminal.models import Applet, VirtualApp def date_expired_default(): @@ -177,6 +177,15 @@ class ConnectionToken(JMSOrgBaseModel): } return options + def get_virtual_app_option(self): + method = self.connect_method_object + if not method or method.get('type') != 'virtual_app' or method.get('disabled', False): + return None + virtual_app = VirtualApp.objects.filter(name=method.get('value')).first() + if not virtual_app: + return None + return virtual_app + def get_applet_option(self): method = self.connect_method_object if not method or method.get('type') != 'applet' or method.get('disabled', False): @@ -190,28 +199,23 @@ class ConnectionToken(JMSOrgBaseModel): if not host_account: raise JMSException({'error': 'No host account available'}) - host, account, lock_key, ttl = bulk_get(host_account, ('host', 'account', 'lock_key', 'ttl')) + host, account, lock_key = bulk_get(host_account, ('host', 'account', 'lock_key')) gateway = host.domain.select_gateway() if host.domain else None data = { - 'id': account.id, + 'id': lock_key, 'applet': applet, 'host': host, 'gateway': gateway, 'account': account, 'remote_app_option': self.get_remote_app_option() } - token_account_relate_key = f'token_account_relate_{account.id}' - cache.set(token_account_relate_key, lock_key, ttl) return data @staticmethod - def release_applet_account(account_id): - token_account_relate_key = f'token_account_relate_{account_id}' - lock_key = cache.get(token_account_relate_key) + def release_applet_account(lock_key): if lock_key: cache.delete(lock_key) - cache.delete(token_account_relate_key) return True @lazyproperty diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py index 6685316bd..3eea79e4e 100644 --- a/apps/authentication/serializers/connect_token_secret.py +++ b/apps/authentication/serializers/connect_token_secret.py @@ -15,7 +15,8 @@ from users.models import User from ..models import ConnectionToken __all__ = [ - 'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer' + 'ConnectionTokenSecretSerializer', 'ConnectTokenAppletOptionSerializer', + 'ConnectTokenVirtualAppOptionSerializer', ] @@ -161,3 +162,10 @@ class ConnectTokenAppletOptionSerializer(serializers.Serializer): account = _ConnectionTokenAccountSerializer(read_only=True) gateway = _ConnectionTokenGatewaySerializer(read_only=True) remote_app_option = serializers.JSONField(read_only=True) + + +class ConnectTokenVirtualAppOptionSerializer(serializers.Serializer): + name = serializers.CharField(label=_('Name')) + image_name = serializers.CharField(label=_('Image name')) + image_port = serializers.IntegerField(label=_('Image port')) + image_protocol = serializers.CharField(label=_('Image protocol')) diff --git a/apps/authentication/templates/authentication/login.html b/apps/authentication/templates/authentication/login.html index 7e644c047..4d7489034 100644 --- a/apps/authentication/templates/authentication/login.html +++ b/apps/authentication/templates/authentication/login.html @@ -286,9 +286,9 @@
diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index e7e561ffd..3bb7898df 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -16,16 +16,9 @@ router.register('super-connection-token', api.SuperConnectionTokenViewSet, 'supe router.register('confirm', api.UserConfirmationViewSet, 'confirm') urlpatterns = [ - path('wecom/qr/unbind/', api.WeComQRUnBindForUserApi.as_view(), name='wecom-qr-unbind'), - path('wecom/qr/unbind//', api.WeComQRUnBindForAdminApi.as_view(), name='wecom-qr-unbind-for-admin'), + path('/qr/unbind/', api.QRUnBindForUserApi.as_view(), name='qr-unbind'), + path('/qr/unbind//', api.QRUnBindForAdminApi.as_view(), name='qr-unbind-for-admin'), - path('dingtalk/qr/unbind/', api.DingTalkQRUnBindForUserApi.as_view(), name='dingtalk-qr-unbind'), - path('dingtalk/qr/unbind//', api.DingTalkQRUnBindForAdminApi.as_view(), - name='dingtalk-qr-unbind-for-admin'), - - path('feishu/qr/unbind/', api.FeiShuQRUnBindForUserApi.as_view(), name='feishu-qr-unbind'), - path('feishu/qr/unbind//', api.FeiShuQRUnBindForAdminApi.as_view(), - name='feishu-qr-unbind-for-admin'), path('feishu/event/subscription/callback/', api.FeiShuEventSubscriptionCallback.as_view(), name='feishu-event-subscription-callback'), diff --git a/apps/authentication/urls/view_urls.py b/apps/authentication/urls/view_urls.py index ea08eddd0..a648ded51 100644 --- a/apps/authentication/urls/view_urls.py +++ b/apps/authentication/urls/view_urls.py @@ -27,7 +27,7 @@ urlpatterns = [ path('wecom/bind/start/', views.WeComEnableStartView.as_view(), name='wecom-bind-start'), path('wecom/qr/bind/', views.WeComQRBindView.as_view(), name='wecom-qr-bind'), path('wecom/qr/login/', views.WeComQRLoginView.as_view(), name='wecom-qr-login'), - path('wecom/qr/bind//callback/', views.WeComQRBindCallbackView.as_view(), + path('wecom/qr/bind/callback/', views.WeComQRBindCallbackView.as_view(), name='wecom-qr-bind-callback'), path('wecom/qr/login/callback/', views.WeComQRLoginCallbackView.as_view(), name='wecom-qr-login-callback'), path('wecom/oauth/login/', views.WeComOAuthLoginView.as_view(), name='wecom-oauth-login'), @@ -49,6 +49,12 @@ urlpatterns = [ path('feishu/qr/bind/callback/', views.FeiShuQRBindCallbackView.as_view(), name='feishu-qr-bind-callback'), path('feishu/qr/login/callback/', views.FeiShuQRLoginCallbackView.as_view(), name='feishu-qr-login-callback'), + path('slack/bind/start/', views.SlackEnableStartView.as_view(), name='slack-bind-start'), + path('slack/qr/bind/', views.SlackQRBindView.as_view(), name='slack-qr-bind'), + path('slack/qr/login/', views.SlackQRLoginView.as_view(), name='slack-qr-login'), + path('slack/qr/bind/callback/', views.SlackQRBindCallbackView.as_view(), name='slack-qr-bind-callback'), + path('slack/qr/login/callback/', views.SlackQRLoginCallbackView.as_view(), name='slack-qr-login-callback'), + # Profile path('profile/pubkey/generate/', users_view.UserPublicKeyGenerateView.as_view(), name='user-pubkey-generate'), path('profile/mfa/', users_view.MFASettingView.as_view(), name='user-mfa-setting'), diff --git a/apps/authentication/views/__init__.py b/apps/authentication/views/__init__.py index 38cf114e1..214c8943a 100644 --- a/apps/authentication/views/__init__.py +++ b/apps/authentication/views/__init__.py @@ -5,3 +5,4 @@ from .mfa import * from .wecom import * from .dingtalk import * from .feishu import * +from .slack import * diff --git a/apps/authentication/views/base.py b/apps/authentication/views/base.py index c118680f0..598ec9a0d 100644 --- a/apps/authentication/views/base.py +++ b/apps/authentication/views/base.py @@ -2,6 +2,7 @@ from functools import lru_cache from django.conf import settings from django.db.utils import IntegrityError +from django.contrib.auth import logout as auth_logout from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django.views import View @@ -9,8 +10,10 @@ from rest_framework.request import Request from authentication import errors from authentication.mixins import AuthMixin +from authentication.notifications import OAuthBindMessage from common.utils import get_logger from common.utils.django import reverse, get_object_or_none +from common.utils.common import get_request_ip from users.models import User from users.signal_handlers import check_only_allow_exist_user_auth from .mixins import FlashMessageMixin @@ -18,9 +21,21 @@ from .mixins import FlashMessageMixin logger = get_logger(__file__) -class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, View): +class IMClientMixin: client_type_path = '' client_auth_params = {} + + @property + @lru_cache(maxsize=1) + def client(self): + if not all([self.client_type_path, self.client_auth_params]): + raise NotImplementedError + client_init = {k: getattr(settings, v) for k, v in self.client_auth_params.items()} + client_type = import_string(self.client_type_path) + return client_type(**client_init) + + +class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, IMClientMixin, View): user_type = '' auth_backend = None # 提示信息 @@ -34,15 +49,6 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, View): def get_verify_state_failed_response(self, redirect_uri): raise NotImplementedError - @property - @lru_cache(maxsize=1) - def client(self): - if not all([self.client_type_path, self.client_auth_params]): - raise NotImplementedError - client_init = {k: getattr(settings, v) for k, v in self.client_auth_params.items()} - client_type = import_string(self.client_type_path) - return client_type(**client_init) - def create_user_if_not_exist(self, user_id, **kwargs): user = None user_attr = self.client.get_user_detail(user_id, **kwargs) @@ -99,3 +105,53 @@ class BaseLoginCallbackView(AuthMixin, FlashMessageMixin, View): response = self.get_failed_response(login_url, title=msg, msg=msg) return response return self.redirect_to_guard_view() + + +class BaseBindCallbackView(FlashMessageMixin, IMClientMixin, View): + auth_type = '' + auth_type_label = '' + + def verify_state(self): + raise NotImplementedError + + def get_verify_state_failed_response(self, redirect_uri): + raise NotImplementedError + + def get_already_bound_response(self, redirect_uri): + raise NotImplementedError + + def get(self, request: Request): + code = request.GET.get('code') + redirect_url = request.GET.get('redirect_url') + + if not self.verify_state(): + return self.get_verify_state_failed_response(redirect_url) + + user = request.user + source_id = getattr(user, f'{self.auth_type}_id', None) + if source_id: + response = self.get_already_bound_response(redirect_url) + return response + + auth_user_id, __ = self.client.get_user_id_by_code(code) + if not auth_user_id: + msg = _('%s query user failed') % self.auth_type_label + response = self.get_failed_response(redirect_url, msg, msg) + return response + + try: + setattr(user, f'{self.auth_type}_id', auth_user_id) + user.save() + except IntegrityError as e: + if e.args[0] == 1062: + msg = _('The %s is already bound to another user') % self.auth_type_label + response = self.get_failed_response(redirect_url, msg, msg) + return response + raise e + + ip = get_request_ip(request) + OAuthBindMessage(user, ip, self.auth_type_label, auth_user_id).publish_async() + msg = _('Binding %s successfully') % self.auth_type_label + auth_logout(request) + response = self.get_success_response(redirect_url, msg, msg) + return response diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 536ef4155..9b7f81e25 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -18,7 +18,7 @@ from authentication.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import get_object_or_none, reverse +from common.utils.django import get_object_or_none, reverse, safe_next_url from common.utils.random import random_string from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from users.models import User @@ -185,6 +185,7 @@ class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') + next_url = safe_next_url(next_url, request=request) redirect_uri = reverse('authentication:dingtalk-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 4ceacc21b..0427c2b1b 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -1,8 +1,6 @@ from urllib.parse import urlencode from django.conf import settings -from django.contrib.auth import logout as auth_logout -from django.db.utils import IntegrityError from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ @@ -11,16 +9,14 @@ from rest_framework.exceptions import APIException from rest_framework.permissions import AllowAny, IsAuthenticated from authentication.const import ConfirmType -from authentication.notifications import OAuthBindMessage from authentication.permissions import UserConfirmation -from common.sdk.im.feishu import URL, FeiShu +from common.sdk.im.feishu import URL from common.utils import get_logger -from common.utils.common import get_request_ip from common.utils.django import reverse from common.utils.random import random_string from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from users.views import UserVerifyPasswordView -from .base import BaseLoginCallbackView +from .base import BaseLoginCallbackView, BaseBindCallbackView from .mixins import FlashMessageMixin logger = get_logger(__file__) @@ -82,49 +78,13 @@ class FeiShuQRBindView(FeiShuQRMixin, View): return HttpResponseRedirect(url) -class FeiShuQRBindCallbackView(FeiShuQRMixin, View): +class FeiShuQRBindCallbackView(FeiShuQRMixin, BaseBindCallbackView): permission_classes = (IsAuthenticated,) - def get(self, request: HttpRequest): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - user = request.user - - if user.feishu_id: - response = self.get_already_bound_response(redirect_url) - return response - - feishu = FeiShu( - app_id=settings.FEISHU_APP_ID, - app_secret=settings.FEISHU_APP_SECRET - ) - user_id, __ = feishu.get_user_id_by_code(code) - - if not user_id: - msg = _('FeiShu query user failed') - response = self.get_failed_response(redirect_url, msg, msg) - return response - - try: - user.feishu_id = user_id - user.save() - except IntegrityError as e: - if e.args[0] == 1062: - msg = _('The FeiShu is already bound to another user') - response = self.get_failed_response(redirect_url, msg, msg) - return response - raise e - - ip = get_request_ip(request) - OAuthBindMessage(user, ip, _('FeiShu'), user_id).publish_async() - msg = _('Binding FeiShu successfully') - auth_logout(request) - response = self.get_success_response(redirect_url, msg, msg) - return response + client_type_path = 'common.sdk.im.feishu.FeiShu' + client_auth_params = {'app_id': 'FEISHU_APP_ID', 'app_secret': 'FEISHU_APP_SECRET'} + auth_type = 'feishu' + auth_type_label = _('FeiShu') class FeiShuEnableStartView(UserVerifyPasswordView): diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 6514e14eb..23ad4d814 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -24,7 +24,7 @@ from django.views.decorators.debug import sensitive_post_parameters from django.views.generic.base import TemplateView, RedirectView from django.views.generic.edit import FormView -from common.utils import FlashMessageUtil, static_or_direct +from common.utils import FlashMessageUtil, static_or_direct, safe_next_url from users.utils import ( redirect_user_first_login_or_index ) @@ -91,6 +91,12 @@ class UserLoginContextMixin: 'url': reverse('authentication:feishu-qr-login'), 'logo': static('img/login_feishu_logo.png') }, + { + 'name': _('Slack'), + 'enabled': settings.AUTH_SLACK, + 'url': reverse('authentication:slack-qr-login'), + 'logo': static('img/login_slack_logo.png') + }, { 'name': _("Passkey"), 'enabled': settings.AUTH_PASSKEY, @@ -202,6 +208,7 @@ class UserLoginView(mixins.AuthMixin, UserLoginContextMixin, FormView): auth_name, redirect_url = auth_method['name'], auth_method['url'] next_url = request.GET.get('next') or '/' + next_url = safe_next_url(next_url, request=request) query_string = request.GET.urlencode() redirect_url = '{}?next={}&{}'.format(redirect_url, next_url, query_string) diff --git a/apps/authentication/views/slack.py b/apps/authentication/views/slack.py new file mode 100644 index 000000000..70edcf4c5 --- /dev/null +++ b/apps/authentication/views/slack.py @@ -0,0 +1,128 @@ +from urllib.parse import urlencode + +from django.conf import settings +from django.http.response import HttpResponseRedirect +from django.utils.translation import gettext_lazy as _ +from django.views import View +from rest_framework.exceptions import APIException +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.request import Request + +from authentication.const import ConfirmType +from authentication.permissions import UserConfirmation +from common.sdk.im.slack import URL, SLACK_REDIRECT_URI_SESSION_KEY +from common.utils import get_logger +from common.utils.django import reverse +from common.utils.random import random_string +from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin +from users.views import UserVerifyPasswordView +from .base import BaseLoginCallbackView, BaseBindCallbackView +from .mixins import FlashMessageMixin + +logger = get_logger(__file__) + +SLACK_STATE_SESSION_KEY = '_slack_state' + + +class SlackMixin(UserConfirmRequiredExceptionMixin, PermissionsMixin, FlashMessageMixin, View): + def dispatch(self, request, *args, **kwargs): + try: + return super().dispatch(request, *args, **kwargs) + except APIException as e: + msg = str(e.detail) + return self.get_failed_response( + '/', + _('Slack Error'), + msg + ) + + def verify_state(self): + state = self.request.GET.get('state') + session_state = self.request.session.get(SLACK_STATE_SESSION_KEY) + if state != session_state: + return False + return True + + def get_verify_state_failed_response(self, redirect_uri): + msg = _("The system configuration is incorrect. Please contact your administrator") + return self.get_failed_response(redirect_uri, msg, msg) + + def get_qr_url(self, redirect_uri): + state = random_string(16) + self.request.session[SLACK_STATE_SESSION_KEY] = state + + params = { + 'client_id': settings.SLACK_CLIENT_ID, + 'state': state, 'scope': 'users:read,users:read.email', + 'redirect_uri': redirect_uri, + } + url = URL().AUTHORIZE + '?' + urlencode(params) + return url + + def get_already_bound_response(self, redirect_url): + msg = _('Slack is already bound') + response = self.get_failed_response(redirect_url, msg, msg) + return response + + +class SlackQRBindView(SlackMixin, View): + permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) + + def get(self, request: Request): + redirect_url = request.GET.get('redirect_url') + + redirect_uri = reverse('authentication:slack-qr-bind-callback', external=True) + redirect_uri += '?' + urlencode({'redirect_url': redirect_url}) + self.request.session[SLACK_REDIRECT_URI_SESSION_KEY] = redirect_uri + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class SlackQRBindCallbackView(SlackMixin, BaseBindCallbackView): + permission_classes = (IsAuthenticated,) + + client_type_path = 'common.sdk.im.slack.Slack' + client_auth_params = {'client_id': 'SLACK_CLIENT_ID', 'client_secret': 'SLACK_CLIENT_SECRET'} + auth_type = 'slack' + auth_type_label = _('Slack') + + +class SlackEnableStartView(UserVerifyPasswordView): + + def get_success_url(self): + referer = self.request.META.get('HTTP_REFERER') + redirect_url = self.request.GET.get("redirect_url") + + success_url = reverse('authentication:slack-qr-bind') + success_url += '?' + urlencode({ + 'redirect_url': redirect_url or referer + }) + + return success_url + + +class SlackQRLoginView(SlackMixin, View): + permission_classes = (AllowAny,) + + def get(self, request: Request): + redirect_url = request.GET.get('redirect_url') or reverse('index') + redirect_uri = reverse('authentication:slack-qr-login-callback', external=True) + redirect_uri += '?' + urlencode({ + 'redirect_url': redirect_url, + }) + self.request.session[SLACK_REDIRECT_URI_SESSION_KEY] = redirect_uri + url = self.get_qr_url(redirect_uri) + return HttpResponseRedirect(url) + + +class SlackQRLoginCallbackView(SlackMixin, BaseLoginCallbackView): + permission_classes = (AllowAny,) + + client_type_path = 'common.sdk.im.slack.Slack' + client_auth_params = {'client_id': 'SLACK_CLIENT_ID', 'client_secret': 'SLACK_CLIENT_SECRET'} + user_type = 'slack' + auth_backend = 'AUTH_BACKEND_SLACK' + + msg_client_err = _('Slack Error') + msg_user_not_bound_err = _('Slack is not bound') + msg_not_found_user_from_client_err = _('Failed to get user from Slack') diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index 6817c84a0..48268e516 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -1,8 +1,6 @@ from urllib.parse import urlencode from django.conf import settings -from django.contrib.auth import logout as auth_logout -from django.db.utils import IntegrityError from django.http.request import HttpRequest from django.http.response import HttpResponseRedirect from django.utils.translation import gettext_lazy as _ @@ -13,18 +11,17 @@ from rest_framework.permissions import IsAuthenticated, AllowAny from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin -from authentication.notifications import OAuthBindMessage from authentication.permissions import UserConfirmation from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom from common.utils import get_logger from common.utils.common import get_request_ip -from common.utils.django import reverse, get_object_or_none +from common.utils.django import reverse, get_object_or_none, safe_next_url from common.utils.random import random_string from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin from users.models import User from users.views import UserVerifyPasswordView -from .base import BaseLoginCallbackView +from .base import BaseLoginCallbackView, BaseBindCallbackView from .mixins import METAMixin, FlashMessageMixin logger = get_logger(__file__) @@ -104,64 +101,21 @@ class WeComQRBindView(WeComQRMixin, View): permission_classes = (IsAuthenticated, UserConfirmation.require(ConfirmType.RELOGIN)) def get(self, request: HttpRequest): - user = request.user redirect_url = request.GET.get('redirect_url') - - redirect_uri = reverse('authentication:wecom-qr-bind-callback', kwargs={'user_id': user.id}, external=True) + redirect_uri = reverse('authentication:wecom-qr-bind-callback', external=True) redirect_uri += '?' + urlencode({'redirect_url': redirect_url}) url = self.get_qr_url(redirect_uri) return HttpResponseRedirect(url) -class WeComQRBindCallbackView(WeComQRMixin, View): +class WeComQRBindCallbackView(WeComQRMixin, BaseBindCallbackView): permission_classes = (IsAuthenticated,) - def get(self, request: HttpRequest, user_id): - code = request.GET.get('code') - redirect_url = request.GET.get('redirect_url') - - if not self.verify_state(): - return self.get_verify_state_failed_response(redirect_url) - - user = get_object_or_none(User, id=user_id) - if user is None: - logger.error(f'WeComQR bind callback error, user_id invalid: user_id={user_id}') - msg = _('Invalid user_id') - response = self.get_failed_response(redirect_url, msg, msg) - return response - - if user.wecom_id: - response = self.get_already_bound_response(redirect_url) - return response - - wecom = WeCom( - corpid=settings.WECOM_CORPID, - corpsecret=settings.WECOM_SECRET, - agentid=settings.WECOM_AGENTID - ) - wecom_userid, __ = wecom.get_user_id_by_code(code) - if not wecom_userid: - msg = _('WeCom query user failed') - response = self.get_failed_response(redirect_url, msg, msg) - return response - - try: - user.wecom_id = wecom_userid - user.save() - except IntegrityError as e: - if e.args[0] == 1062: - msg = _('The WeCom is already bound to another user') - response = self.get_failed_response(redirect_url, msg, msg) - return response - raise e - - ip = get_request_ip(request) - OAuthBindMessage(user, ip, _('WeCom'), wecom_userid).publish_async() - msg = _('Binding WeCom successfully') - auth_logout(request) - response = self.get_success_response(redirect_url, msg, msg) - return response + client_type_path = 'common.sdk.im.wecom.WeCom' + client_auth_params = {'corpid': 'WECOM_CORPID', 'corpsecret': 'WECOM_SECRET', 'agentid': 'WECOM_AGENTID'} + auth_type = 'wecom' + auth_type_label = _('Wecom') class WeComEnableStartView(UserVerifyPasswordView): @@ -182,6 +136,7 @@ class WeComQRLoginView(WeComQRMixin, METAMixin, View): def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') + next_url = safe_next_url(next_url, request=request) redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ 'redirect_url': redirect_url, diff --git a/apps/common/api/mixin.py b/apps/common/api/mixin.py index d93459104..c87d5f22a 100644 --- a/apps/common/api/mixin.py +++ b/apps/common/api/mixin.py @@ -11,7 +11,7 @@ from rest_framework.settings import api_settings from common.drf.filters import ( IDSpmFilterBackend, CustomFilterBackend, IDInFilterBackend, - IDNotFilterBackend, NotOrRelFilterBackend + IDNotFilterBackend, NotOrRelFilterBackend, LabelFilterBackend ) from common.utils import get_logger, lazyproperty from .action import RenderToJsonMixin @@ -111,7 +111,7 @@ class ExtraFilterFieldsMixin: """ default_added_filters = ( CustomFilterBackend, IDSpmFilterBackend, IDInFilterBackend, - IDNotFilterBackend, + IDNotFilterBackend, LabelFilterBackend ) filter_backends = api_settings.DEFAULT_FILTER_BACKENDS extra_filter_fields = [] diff --git a/apps/common/decorators.py b/apps/common/decorators.py index 353407ab1..24b2695d3 100644 --- a/apps/common/decorators.py +++ b/apps/common/decorators.py @@ -73,6 +73,7 @@ executor = ThreadPoolExecutor( ) _loop_debouncer_func_task_cache = {} _loop_debouncer_func_args_cache = {} +_loop_debouncer_func_task_time_cache = {} def get_loop(): @@ -92,6 +93,17 @@ def cancel_or_remove_debouncer_task(cache_key): def run_debouncer_func(cache_key, org, ttl, func, *args, **kwargs): cancel_or_remove_debouncer_task(cache_key) run_func_partial = functools.partial(_run_func_with_org, cache_key, org, func) + + current = time.time() + first_run_time = _loop_debouncer_func_task_time_cache.get(cache_key, None) + if first_run_time is None: + _loop_debouncer_func_task_time_cache[cache_key] = current + first_run_time = current + + if current - first_run_time > ttl: + executor.submit(run_func_partial, *args, **kwargs) + return + loop = _loop_thread.get_loop() _debouncer = Debouncer(run_func_partial, lambda: True, ttl, loop=loop, executor=executor) task = asyncio.run_coroutine_threadsafe(_debouncer(*args, **kwargs), loop=loop) @@ -130,6 +142,7 @@ def _run_func_with_org(key, org, func, *args, **kwargs): logger.error('delay run error: %s' % e) _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) def delay_run(ttl=5, key=None): @@ -142,6 +155,9 @@ def delay_run(ttl=5, key=None): def inner(func): suffix_key_func = key if key else default_suffix_key + sigs = inspect.signature(func) + if len(sigs.parameters) != 0: + raise ValueError('Merge delay run must not arguments: %s' % func.__name__) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -186,12 +202,11 @@ def merge_delay_run(ttl=5, key=None): for k, v in kwargs.items(): if not isinstance(v, (tuple, list, set)): raise ValueError('func kwargs value must be list or tuple: %s %s' % (func.__name__, v)) + v = set(v) if k not in cache_kwargs: cache_kwargs[k] = v - elif isinstance(v, set): - cache_kwargs[k] = cache_kwargs[k].union(v) else: - cache_kwargs[k] = list(cache_kwargs[k]) + list(v) + cache_kwargs[k] = cache_kwargs[k].union(v) _loop_debouncer_func_args_cache[cache_key] = cache_kwargs run_debouncer_func(cache_key, org, ttl, func, *args, **cache_kwargs) @@ -201,8 +216,8 @@ def merge_delay_run(ttl=5, key=None): @delay_run(ttl=5) -def test_delay_run(username): - print("Hello, %s, now is %s" % (username, time.time())) +def test_delay_run(): + print("Hello, now is %s" % time.time()) @merge_delay_run(ttl=5, key=lambda users=(): users[0][0]) diff --git a/apps/common/drf/filters.py b/apps/common/drf/filters.py index eb6878019..0fb8ca63a 100644 --- a/apps/common/drf/filters.py +++ b/apps/common/drf/filters.py @@ -6,6 +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_filters import rest_framework as drf_filters from rest_framework import filters from rest_framework.compat import coreapi, coreschema @@ -21,7 +22,7 @@ __all__ = [ "DatetimeRangeFilterBackend", "IDSpmFilterBackend", 'IDInFilterBackend', "CustomFilterBackend", "BaseFilterSet", 'IDNotFilterBackend', - 'NotOrRelFilterBackend', + 'NotOrRelFilterBackend', 'LabelFilterBackend', ] @@ -168,6 +169,73 @@ class IDNotFilterBackend(filters.BaseFilterBackend): return queryset +class LabelFilterBackend(filters.BaseFilterBackend): + def get_schema_fields(self, view): + return [ + coreapi.Field( + name='label', location='query', required=False, + type='string', example='/api/v1/users/users?label=abc', + description='Filter by label' + ) + ] + + @staticmethod + def filter_resources(resources, labels_id): + label_ids = [i.strip() for i in labels_id.split(',')] + + args = [] + for label_id in label_ids: + kwargs = {} + if ':' in label_id: + k, v = label_id.split(':', 1) + kwargs['label__name'] = k.strip() + if v != '*': + kwargs['label__value'] = v.strip() + else: + kwargs['label_id'] = label_id + args.append(kwargs) + + if len(args) == 1: + resources = resources.filter(**args[0]) + return resources + + 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 + + def filter_queryset(self, request, queryset, view): + labels_id = request.query_params.get('labels') + if not labels_id: + return queryset + + if not hasattr(queryset, 'model'): + return queryset + + if not hasattr(queryset.model, 'labels'): + return queryset + + model = queryset.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) + res_ids = resources.values_list('res_id', flat=True) + queryset = queryset.filter(id__in=set(res_ids)) + return queryset + + class CustomFilterBackend(filters.BaseFilterBackend): def get_schema_fields(self, view): diff --git a/apps/common/drf/parsers/base.py b/apps/common/drf/parsers/base.py index 6d4b70788..33b344457 100644 --- a/apps/common/drf/parsers/base.py +++ b/apps/common/drf/parsers/base.py @@ -9,7 +9,7 @@ from rest_framework import status from rest_framework.exceptions import ParseError, APIException from rest_framework.parsers import BaseParser -from common.serializers.fields import ObjectRelatedField +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from common.utils import get_logger logger = get_logger(__file__) @@ -126,6 +126,10 @@ class BaseFileParser(BaseParser): value = [self.id_name_to_obj(v) for v in value] else: value = self.id_name_to_obj(value) + elif isinstance(field, LabeledChoiceField): + value = self.id_name_to_obj(value) + if isinstance(value, dict) and value.get('pk'): + value = value.get('pk') elif isinstance(field, serializers.ListSerializer): value = [self.parse_value(field.child, v) for v in value] elif isinstance(field, serializers.Serializer): diff --git a/apps/common/drf/renders/base.py b/apps/common/drf/renders/base.py index cdc1626ff..2c1038610 100644 --- a/apps/common/drf/renders/base.py +++ b/apps/common/drf/renders/base.py @@ -105,7 +105,8 @@ class BaseFileRenderer(BaseRenderer): elif isinstance(value, bool): value = 'Yes' if value else 'No' elif isinstance(field, LabeledChoiceField): - value = value.get('value', '') + value = value or {} + value = '{}({})'.format(value.get('label'), value.get('value')) elif isinstance(field, ObjectRelatedField): if field.many: value = [self.to_id_name(v) for v in value] diff --git a/apps/common/plugins/es.py b/apps/common/plugins/es.py index 297f2e32a..8e2126f85 100644 --- a/apps/common/plugins/es.py +++ b/apps/common/plugins/es.py @@ -228,6 +228,18 @@ class ES(object): datetime_range['lte'] = datetime__lte return 'datetime', datetime_range + @staticmethod + def handle_exact_fields(exact): + _filter = [] + for k, v in exact.items(): + query = 'term' + if isinstance(v, list): + query = 'terms' + _filter.append({ + query: {k: v} + }) + return _filter + def get_query_body(self, **kwargs): new_kwargs = {} for k, v in kwargs.items(): @@ -235,6 +247,8 @@ class ES(object): v = str(v) if k == 'pk': k = 'id' + if k.endswith('__in'): + k = k.replace('__in', '') new_kwargs[k] = v kwargs = new_kwargs @@ -287,11 +301,8 @@ class ES(object): {'match': {k: v}} for k, v in match.items() ], 'should': should, - 'filter': [ - { - 'term': {k: v} - } for k, v in exact.items() - ] + [ + 'filter': self.handle_exact_fields(exact) + + [ { 'range': { time_field_name: time_range diff --git a/apps/common/sdk/im/slack/__init__.py b/apps/common/sdk/im/slack/__init__.py new file mode 100644 index 000000000..bea92e78e --- /dev/null +++ b/apps/common/sdk/im/slack/__init__.py @@ -0,0 +1,152 @@ +import requests +import mistune + +from rest_framework.exceptions import APIException +from django.utils.translation import gettext_lazy as _ + +from users.utils import construct_user_email +from common.utils.common import get_logger +from jumpserver.utils import get_current_request + +logger = get_logger(__name__) + + +SLACK_REDIRECT_URI_SESSION_KEY = '_slack_redirect_uri' + + +class URL: + AUTHORIZE = 'https://slack.com/oauth/v2/authorize' + ACCESS_TOKEN = 'https://slack.com/api/oauth.v2.access' + GET_USER_INFO_BY_USER_ID = 'https://slack.com/api/users.info' + SEND_MESSAGE = 'https://slack.com/api/chat.postMessage' + AUTH_TEST = 'https://slack.com/api/auth.test' + + +class SlackRenderer(mistune.Renderer): + def header(self, text, level, raw=None): + return '*' + text + '*\n' + + def double_emphasis(self, text): + return '*' + text + '*' + + def list(self, body, ordered=True): + lines = body.split('\n') + for i, line in enumerate(lines): + if not line: + continue + prefix = '• ' + lines[i] = prefix + line[4:-5] + return '\n'.join(lines) + + def block_code(self, code, lang=None): + return f'`{code}`' + + def link(self, link, title, content): + if title or content: + label = str(title or content).strip() + return f'<{link}|{label}>' + return f'<{link}>' + + def paragraph(self, text): + return f'{text.strip()}\n' + + def linebreak(self): + return '\n' + + +class SlackRequests: + def __init__(self, client_id=None, client_secret=None, bot_token=None): + self._client_id = client_id + self._client_secret = client_secret + self._bot_token = bot_token + self.access_token = None + self.user_id = None + + def add_token(self, headers, with_bot_token, with_access_token): + if with_access_token: + headers.update({'Authorization': f'Bearer {self.access_token}'}) + if with_bot_token: + headers.update({'Authorization': f'Bearer {self._bot_token}'}) + + def request(self, method, url, with_bot_token=True, with_access_token=False, **kwargs): + headers = kwargs.pop('headers', {}) + self.add_token(headers, with_bot_token=with_bot_token, with_access_token=with_access_token) + + func_handler = getattr(requests, method, requests.get) + data = func_handler(url, headers=headers, **kwargs).json() + if not data.get('ok'): + raise APIException( + detail=data.get('error', _('Unknown error occur')) + ) + return data + + def request_access_token(self, code): + request = get_current_request() + data = { + 'code': code, 'client_id': self._client_id, 'client_secret': self._client_secret, + 'grant_type': 'authorization_code', + 'redirect_uri': request.session.get(SLACK_REDIRECT_URI_SESSION_KEY) + } + response = self.request( + 'post', url=URL().ACCESS_TOKEN, data=data, with_bot_token=False + ) + self.access_token = response['access_token'] + self.user_id = response['authed_user']['id'] + + +class Slack: + def __init__(self, client_id=None, client_secret=None, bot_token=None, **kwargs): + self._client = SlackRequests( + client_id=client_id, client_secret=client_secret, bot_token=bot_token + ) + self.markdown = mistune.Markdown(renderer=SlackRenderer()) + + def get_user_id_by_code(self, code): + self._client.request_access_token(code) + response = self._client.request( + 'get', f'{URL().GET_USER_INFO_BY_USER_ID}?user={self._client.user_id}', + with_bot_token=False, with_access_token=True + ) + return self._client.user_id, response['user'] + + def is_valid(self): + return self._client.request('post', URL().AUTH_TEST) + + def convert_to_markdown(self, message): + blocks = [] + for line in message.split('\n'): + block = self.markdown(line) + if not block: + continue + if block.startswith('
'): + block_item = {'type': 'divider'} + else: + block_item = { + "type": "section", + "text": {"type": "mrkdwn", "text": block} + } + blocks.append(block_item) + return {'blocks': blocks} + + def send_text(self, user_ids, msg_body): + body = self.convert_to_markdown(msg_body) + logger.info(f'Slack send text: user_ids={user_ids} msg={body}') + for user_id in user_ids: + body['channel'] = user_id + try: + self._client.request('post', URL().SEND_MESSAGE, json=body) + except APIException as e: + # 只处理可预知的错误 + logger.exception(e) + + @staticmethod + def get_user_detail(user_id, **kwargs): + # get_user_id_by_code 已经返回个人信息,这里直接解析 + user_info = kwargs['other_info'] + username = user_info.get('name') or user_id + name = user_info.get('real_name', username) + email = user_info.get('profile', {}).get('email') + email = construct_user_email(username, email) + return { + 'username': username, 'name': name, 'email': email + } diff --git a/apps/common/sdk/sms/endpoint.py b/apps/common/sdk/sms/endpoint.py index 6f5433fa5..7618d6da9 100644 --- a/apps/common/sdk/sms/endpoint.py +++ b/apps/common/sdk/sms/endpoint.py @@ -1,12 +1,12 @@ -from collections import OrderedDict import importlib +from collections import OrderedDict -from django.utils.translation import gettext_lazy as _ -from django.db.models import TextChoices from django.conf import settings +from django.db.models import TextChoices +from django.utils.translation import gettext_lazy as _ -from common.utils import get_logger from common.exceptions import JMSException +from common.utils import get_logger from .base import BaseSMSClient logger = get_logger(__name__) @@ -18,7 +18,7 @@ class BACKENDS(TextChoices): HUAWEI = 'huawei', _('Huawei Cloud') CMPP2 = 'cmpp2', _('CMPP v2.0') CUSTOM = 'custom', _('Custom type') - CUSTOM_FILE = 'custom_file', f"{_('Custom type')}({_('File')})" + CUSTOM_FILE = 'custom_file', _('Custom type (File)') class SMS: diff --git a/apps/common/serializers/fields.py b/apps/common/serializers/fields.py index 5fe019c65..282394106 100644 --- a/apps/common/serializers/fields.py +++ b/apps/common/serializers/fields.py @@ -20,7 +20,8 @@ __all__ = [ "TreeChoicesField", "LabeledMultipleChoiceField", "PhoneField", - "JSONManyToManyField" + "JSONManyToManyField", + "LabelRelatedField", ] @@ -56,21 +57,18 @@ class EncryptedField(serializers.CharField): class LabeledChoiceField(ChoiceField): - def __init__(self, *args, **kwargs): - super(LabeledChoiceField, self).__init__(*args, **kwargs) - self.choice_mapper = { - key: value for key, value in self.choices.items() - } - def to_representation(self, key): if key is None: return key - label = self.choice_mapper.get(key, key) + label = self.choices.get(key, key) return {"value": key, "label": label} def to_internal_value(self, data): if isinstance(data, dict): data = data.get("value") + + if isinstance(data, str) and "(" in data and data.endswith(")"): + data = data.strip(")").split('(')[-1] return super(LabeledChoiceField, self).to_internal_value(data) @@ -99,6 +97,37 @@ class LabeledMultipleChoiceField(serializers.MultipleChoiceField): return data +class LabelRelatedField(serializers.RelatedField): + def __init__(self, **kwargs): + queryset = kwargs.pop("queryset", None) + if queryset is None: + from labels.models import LabeledResource + queryset = LabeledResource.objects.all() + + kwargs = {**kwargs} + read_only = kwargs.get("read_only", False) + if not read_only: + kwargs["queryset"] = queryset + super().__init__(**kwargs) + + def to_representation(self, value): + if value is None: + return value + return str(value.label) + + def to_internal_value(self, data): + from labels.models import LabeledResource, Label + if data is None: + return data + if isinstance(data, dict): + pk = data.get("id") or data.get("pk") + label = Label.objects.get(pk=pk) + else: + k, v = data.split(":", 1) + label, __ = Label.objects.get_or_create(name=k, value=v, defaults={'name': k, 'value': v}) + return LabeledResource(label=label) + + class ObjectRelatedField(serializers.RelatedField): default_error_messages = { "required": _("This field is required."), diff --git a/apps/common/serializers/mixin.py b/apps/common/serializers/mixin.py index d665d6346..d51849f07 100644 --- a/apps/common/serializers/mixin.py +++ b/apps/common/serializers/mixin.py @@ -7,6 +7,7 @@ else: from collections import Iterable from django.core.exceptions import ObjectDoesNotExist from django.db.models import NOT_PROVIDED +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField, empty @@ -14,14 +15,13 @@ from rest_framework.settings import api_settings from rest_framework.utils import html from common.db.fields import EncryptMixin -from common.serializers.fields import EncryptedField, LabeledChoiceField, ObjectRelatedField +from common.serializers.fields import EncryptedField, LabeledChoiceField, ObjectRelatedField, LabelRelatedField __all__ = [ 'BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin', 'SecretReadableMixin', 'CommonModelSerializer', - 'CommonBulkModelSerializer', - + 'CommonBulkModelSerializer', 'ResourceLabelsMixin', ] @@ -391,3 +391,25 @@ class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): class CommonBulkModelSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): pass + + +class ResourceLabelsMixin(serializers.Serializer): + labels = LabelRelatedField(many=True, label=_('Labels'), required=False, allow_null=True) + + def update(self, instance, validated_data): + labels = validated_data.pop('labels', None) + res = super().update(instance, validated_data) + if labels is not None: + instance.labels.set(labels, bulk=False) + return res + + def create(self, validated_data): + labels = validated_data.pop('labels', None) + instance = super().create(validated_data) + if labels is not None: + instance.labels.set(labels, bulk=False) + return instance + + @classmethod + def setup_eager_loading(cls, queryset): + return queryset.prefetch_related('labels') diff --git a/apps/common/signal_handlers.py b/apps/common/signal_handlers.py index df2019b60..588a63632 100644 --- a/apps/common/signal_handlers.py +++ b/apps/common/signal_handlers.py @@ -66,6 +66,10 @@ def digest_sql_query(): for table_name, queries in table_queries.items(): if table_name.startswith('rbac_') or table_name.startswith('auth_permission'): continue + + for query in queries: + sql = query['sql'] + print(" # {}: {}".format(query['time'], sql)) if len(queries) < 3: continue print("- Table: {}".format(table_name)) diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py index add6b79b3..798b4a94c 100644 --- a/apps/common/utils/common.py +++ b/apps/common/utils/common.py @@ -155,10 +155,6 @@ def is_uuid(seq): def get_request_ip(request): - x_real_ip = request.META.get('HTTP_X_REAL_IP', '') - if x_real_ip: - return x_real_ip - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') if x_forwarded_for and x_forwarded_for[0]: login_ip = x_forwarded_for[0] diff --git a/apps/common/utils/django.py b/apps/common/utils/django.py index ad8a218d8..fc692bc15 100644 --- a/apps/common/utils/django.py +++ b/apps/common/utils/django.py @@ -7,6 +7,7 @@ from django.db import models from django.db.models.signals import post_save, pre_save from django.shortcuts import reverse as dj_reverse from django.utils import timezone +from django.utils.http import url_has_allowed_host_and_scheme UUID_PATTERN = re.compile(r'[0-9a-zA-Z\-]{36}') @@ -94,3 +95,12 @@ def get_request_os(request): return 'linux' else: return 'unknown' + + +def safe_next_url(next_url, request=None): + safe_hosts = [*settings.ALLOWED_HOSTS] + if request: + safe_hosts.append(request.get_host()) + if not next_url or not url_has_allowed_host_and_scheme(next_url, safe_hosts): + next_url = '/' + return next_url diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index 505fbd041..7978267b9 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -32,7 +32,16 @@ def random_replace_char(s, chars, length): return ''.join(seq) -def random_string(length: int, lower=True, upper=True, digit=True, special_char=False, symbols=string_punctuation): +def remove_exclude_char(s, exclude_chars): + for i in exclude_chars: + s = s.replace(i, '') + return s + + +def random_string( + length: int, lower=True, upper=True, digit=True, + special_char=False, exclude_chars='', symbols=string_punctuation +): if not any([lower, upper, digit]): raise ValueError('At least one of `lower`, `upper`, `digit` must be `True`') if length < 4: @@ -44,11 +53,13 @@ def random_string(length: int, lower=True, upper=True, digit=True, special_char= (digit, string.digits), ) chars = ''.join([i[1] for i in chars_map if i[0]]) + chars = remove_exclude_char(chars, exclude_chars) texts = list(secrets.choice(chars) for __ in range(length)) texts = ''.join(texts) # 控制一下特殊字符的数量, 别随机出来太多 if special_char: + symbols = remove_exclude_char(symbols, exclude_chars) symbol_num = length // 16 + 1 texts = random_replace_char(texts, symbols, symbol_num) return texts diff --git a/apps/common/utils/yml.py b/apps/common/utils/yml.py index 6db03ea63..4250f016e 100644 --- a/apps/common/utils/yml.py +++ b/apps/common/utils/yml.py @@ -5,20 +5,21 @@ from django.conf import settings from jinja2 import Environment -def translate(key, i18n): - lang = settings.LANGUAGE_CODE[:2] +def translate(key, i18n, lang): + lang = settings.LANGUAGE_CODE if lang is None else lang + lang = lang[:2] lang_data = i18n.get(key, {}) return lang_data.get(lang, key) -def yaml_load_with_i18n(stream): +def yaml_load_with_i18n(stream, lang=None): ori_text = stream.read() stream = io.StringIO(ori_text) yaml_data = yaml.safe_load(stream) i18n = yaml_data.get('i18n', {}) env = Environment() - env.filters['trans'] = lambda key: translate(key, i18n) + env.filters['trans'] = lambda key: translate(key, i18n, lang) template = env.from_string(ori_text) yaml_data = template.render() yaml_f = io.StringIO(yaml_data) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index ea33d178a..80790eb2e 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -405,6 +405,12 @@ class Config(dict): 'FEISHU_APP_SECRET': '', 'FEISHU_VERSION': 'feishu', + # Slack + 'AUTH_SLACK': False, + 'SLACK_CLIENT_ID': '', + 'SLACK_CLIENT_SECRET': '', + 'SLACK_BOT_TOKEN': '', + 'LOGIN_REDIRECT_TO_BACKEND': '', # 'OPENID / CAS / SAML2 'LOGIN_REDIRECT_MSG_ENABLED': True, @@ -497,7 +503,7 @@ class Config(dict): 'SECURITY_LUNA_REMEMBER_AUTH': True, 'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_MFA_VERIFY_TTL': 3600, - 'SECURITY_UNCOMMON_USERS_TTL': 90, + 'SECURITY_UNCOMMON_USERS_TTL': 999, 'VERIFY_CODE_TTL': 60, 'SECURITY_SESSION_SHARE': True, 'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True, @@ -578,12 +584,23 @@ class Config(dict): 'APPLET_DOWNLOAD_HOST': '', # FTP 文件上传下载备份阈值,单位(M),当值小于等于0时,不备份 - 'FTP_FILE_MAX_STORE': 100, + 'FTP_FILE_MAX_STORE': 0, - # API 请求次数限制 - 'MAX_LIMIT_PER_PAGE': 100, + # API 分页 + 'MAX_LIMIT_PER_PAGE': 10000, + 'DEFAULT_PAGE_SIZE': None, 'LIMIT_SUPER_PRIV': False, + + # Chat AI + 'CHAT_AI_ENABLED': False, + 'GPT_API_KEY': '', + 'GPT_BASE_URL': '', + 'GPT_PROXY': '', + 'GPT_MODEL': 'gpt-3.5-turbo', + 'VIRTUAL_APP_ENABLED': False, + + 'FILE_UPLOAD_SIZE_LIMIT_MB': 200 } old_config_map = { diff --git a/apps/jumpserver/context_processor.py b/apps/jumpserver/context_processor.py index 7b92674bb..24a96b619 100644 --- a/apps/jumpserver/context_processor.py +++ b/apps/jumpserver/context_processor.py @@ -12,6 +12,8 @@ default_interface = dict(( ('login_title', _('JumpServer Open Source Bastion Host')), ('theme', 'classic_green'), ('theme_info', {}), + ('beian_link', ''), + ('beian_text', '') )) default_context = { diff --git a/apps/jumpserver/rewriting/db.py b/apps/jumpserver/rewriting/db.py index 1ccd49d8f..c14d6bf3d 100644 --- a/apps/jumpserver/rewriting/db.py +++ b/apps/jumpserver/rewriting/db.py @@ -26,7 +26,7 @@ class OneToOneField(models.OneToOneField, ForeignKey): def set_db_constraint(): if os.getenv('DB_CONSTRAINT', '1') != '0': return - if len(sys.argv) == 2 and sys.argv[1] == 'makemigrations': + if len(sys.argv) >= 2 and sys.argv[1] in ['makemigrations', 'check']: return print("Set foreignkey db constraint False") transaction.atomic = atomic diff --git a/apps/jumpserver/rewriting/pagination.py b/apps/jumpserver/rewriting/pagination.py index a924d854a..9a5fd263d 100644 --- a/apps/jumpserver/rewriting/pagination.py +++ b/apps/jumpserver/rewriting/pagination.py @@ -1,6 +1,20 @@ from django.conf import settings +from django.core.exceptions import FieldError from rest_framework.pagination import LimitOffsetPagination class MaxLimitOffsetPagination(LimitOffsetPagination): max_limit = settings.MAX_LIMIT_PER_PAGE + + def get_count(self, queryset): + try: + return queryset.values_list('id').order_by().count() + except (AttributeError, TypeError, FieldError): + return len(queryset) + + def paginate_queryset(self, queryset, request, view=None): + if view and hasattr(view, 'page_max_limit'): + self.max_limit = view.page_max_limit + if view and hasattr(view, 'page_default_limit'): + self.default_limit = view.page_default_limit + return super().paginate_queryset(queryset, request, view) diff --git a/apps/jumpserver/rewriting/storage/permissions.py b/apps/jumpserver/rewriting/storage/permissions.py index 7af0adece..511545865 100644 --- a/apps/jumpserver/rewriting/storage/permissions.py +++ b/apps/jumpserver/rewriting/storage/permissions.py @@ -5,6 +5,7 @@ path_perms_map = { 'settings': '*', 'replay': 'default', 'applets': 'terminal.view_applet', + 'virtual_apps': 'terminal.view_virtualapp', 'playbooks': 'ops.view_playbook' } diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 5b6a1cd35..94c45b135 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -140,6 +140,12 @@ FEISHU_APP_ID = CONFIG.FEISHU_APP_ID FEISHU_APP_SECRET = CONFIG.FEISHU_APP_SECRET FEISHU_VERSION = CONFIG.FEISHU_VERSION +# Slack auth +AUTH_SLACK = CONFIG.AUTH_SLACK +SLACK_CLIENT_ID = CONFIG.SLACK_CLIENT_ID +SLACK_CLIENT_SECRET = CONFIG.SLACK_CLIENT_SECRET +SLACK_BOT_TOKEN = CONFIG.SLACK_BOT_TOKEN + # Saml2 auth AUTH_SAML2 = CONFIG.AUTH_SAML2 AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT = CONFIG.AUTH_SAML2_PROVIDER_AUTHORIZATION_ENDPOINT @@ -201,6 +207,7 @@ AUTH_BACKEND_SSO = 'authentication.backends.sso.SSOAuthentication' AUTH_BACKEND_WECOM = 'authentication.backends.sso.WeComAuthentication' AUTH_BACKEND_DINGTALK = 'authentication.backends.sso.DingTalkAuthentication' AUTH_BACKEND_FEISHU = 'authentication.backends.sso.FeiShuAuthentication' +AUTH_BACKEND_SLACK = 'authentication.backends.sso.SlackAuthentication' AUTH_BACKEND_AUTH_TOKEN = 'authentication.backends.sso.AuthorizationTokenAuthentication' AUTH_BACKEND_SAML2 = 'authentication.backends.saml2.SAML2Backend' AUTH_BACKEND_OAUTH2 = 'authentication.backends.oauth2.OAuth2Backend' @@ -216,7 +223,7 @@ AUTHENTICATION_BACKENDS = [ AUTH_BACKEND_CAS, AUTH_BACKEND_OIDC_PASSWORD, AUTH_BACKEND_OIDC_CODE, AUTH_BACKEND_SAML2, AUTH_BACKEND_OAUTH2, # 扫码模式 - AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, + AUTH_BACKEND_WECOM, AUTH_BACKEND_DINGTALK, AUTH_BACKEND_FEISHU, AUTH_BACKEND_SLACK, # Token模式 AUTH_BACKEND_AUTH_TOKEN, AUTH_BACKEND_SSO, AUTH_BACKEND_TEMP_TOKEN, AUTH_BACKEND_PASSKEY diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index 4da1dfc7e..e2bfbc5c1 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -109,6 +109,7 @@ for host_port in ALLOWED_DOMAINS: continue CSRF_TRUSTED_ORIGINS.append('{}://*.{}'.format(schema, origin)) +CORS_ALLOWED_ORIGINS = [o.replace('*.', '') for o in CSRF_TRUSTED_ORIGINS] CSRF_FAILURE_VIEW = 'jumpserver.views.other.csrf_failure' # print("CSRF_TRUSTED_ORIGINS: ") # for origin in CSRF_TRUSTED_ORIGINS: @@ -134,6 +135,7 @@ INSTALLED_APPS = [ 'acls.apps.AclsConfig', 'notifications.apps.NotificationsConfig', 'rbac.apps.RBACConfig', + 'labels.apps.LabelsConfig', 'rest_framework', 'rest_framework_swagger', 'drf_yasg', @@ -142,6 +144,7 @@ INSTALLED_APPS = [ 'django_filters', 'bootstrap3', 'captcha', + 'corsheaders', 'private_storage', 'django_celery_beat', 'django.contrib.auth', @@ -160,6 +163,7 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index dd75f56ec..fad5f0e9d 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -207,7 +207,19 @@ SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key' OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE +DEFAULT_PAGE_SIZE = CONFIG.DEFAULT_PAGE_SIZE # Magnus DB Port MAGNUS_ORACLE_PORTS = CONFIG.MAGNUS_ORACLE_PORTS LIMIT_SUPER_PRIV = CONFIG.LIMIT_SUPER_PRIV + +# Chat AI +CHAT_AI_ENABLED = CONFIG.CHAT_AI_ENABLED +GPT_API_KEY = CONFIG.GPT_API_KEY +GPT_BASE_URL = CONFIG.GPT_BASE_URL +GPT_PROXY = CONFIG.GPT_PROXY +GPT_MODEL = CONFIG.GPT_MODEL + +VIRTUAL_APP_ENABLED = CONFIG.VIRTUAL_APP_ENABLED + +FILE_UPLOAD_SIZE_LIMIT_MB = CONFIG.FILE_UPLOAD_SIZE_LIMIT_MB diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 3679320c4..1f2c333ec 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # import os -from urllib.parse import urlencode from .base import ( REDIS_SSL_CA, REDIS_SSL_CERT, REDIS_SSL_KEY, REDIS_SSL_REQUIRED, REDIS_USE_SSL, @@ -47,6 +46,7 @@ REST_FRAMEWORK = { 'DATETIME_FORMAT': '%Y/%m/%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['%Y/%m/%d %H:%M:%S %z', 'iso-8601', '%Y-%m-%d %H:%M:%S %z'], 'DEFAULT_PAGINATION_CLASS': 'jumpserver.rewriting.pagination.MaxLimitOffsetPagination', + 'PAGE_SIZE': CONFIG.DEFAULT_PAGE_SIZE, 'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler', } @@ -88,7 +88,6 @@ REDIS_LAYERS_HOST = { REDIS_LAYERS_SSL_PARAMS = {} if REDIS_USE_SSL: REDIS_LAYERS_SSL_PARAMS.update({ - 'ssl': REDIS_USE_SSL, 'ssl_cert_reqs': REDIS_SSL_REQUIRED, "ssl_keyfile": REDIS_SSL_KEY, "ssl_certfile": REDIS_SSL_CERT, @@ -115,9 +114,7 @@ else: protocol=REDIS_PROTOCOL, password=CONFIG.REDIS_PASSWORD, host=CONFIG.REDIS_HOST, port=CONFIG.REDIS_PORT, db=CONFIG.REDIS_DB_WS ) - REDIS_LAYERS_SSL_PARAMS.pop('ssl', None) - REDIS_LAYERS_HOST['address'] = '{}?{}'.format(REDIS_LAYERS_ADDRESS, - urlencode(REDIS_LAYERS_SSL_PARAMS)) + REDIS_LAYERS_HOST['address'] = REDIS_LAYERS_ADDRESS CHANNEL_LAYERS = { 'default': { diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 8fbea643c..881d5c57a 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -28,6 +28,7 @@ api_v1 = [ path('acls/', include('acls.urls.api_urls', namespace='api-acls')), path('notifications/', include('notifications.urls.api_urls', namespace='api-notifications')), path('rbac/', include('rbac.urls.api_urls', namespace='api-rbac')), + path('labels/', include('labels.urls', namespace='api-label')), path('prometheus/metrics/', api.PrometheusMetricsApi.as_view()), ] diff --git a/apps/labels/__init__.py b/apps/labels/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/labels/admin.py b/apps/labels/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/labels/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/labels/api.py b/apps/labels/api.py new file mode 100644 index 000000000..44a6702e7 --- /dev/null +++ b/apps/labels/api.py @@ -0,0 +1,151 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.decorators import action +from rest_framework.response import Response + +from common.api.generic import JMSModelViewSet +from orgs.mixins.api import OrgBulkModelViewSet +from orgs.mixins.models import OrgModelMixin +from orgs.utils import current_org +from rbac.models import ContentType +from rbac.serializers import ContentTypeSerializer +from . import serializers +from .const import label_resource_types +from .models import Label, LabeledResource + +__all__ = ['LabelViewSet', 'ContentTypeViewSet'] + + +class ContentTypeViewSet(JMSModelViewSet): + serializer_class = ContentTypeSerializer + http_method_names = ['get', 'head', 'options'] + rbac_perms = { + 'default': 'labels.view_contenttype', + 'resources': 'labels.view_contenttype', + } + page_default_limit = None + can_labeled_content_type = [] + model = ContentType + + def get_queryset(self): + return label_resource_types + + @action(methods=['GET'], detail=True, serializer_class=serializers.ContentTypeResourceSerializer) + def resources(self, request, *args, **kwargs): + self.page_default_limit = 100 + content_type = self.get_object() + model = content_type.model_class() + + if issubclass(model, OrgModelMixin): + queryset = model.objects.filter(org_id=current_org.id) + elif hasattr(model, 'get_queryset'): + queryset = model.get_queryset() + else: + queryset = model.objects.all() + + keyword = request.query_params.get('search') + if keyword: + queryset = content_type.filter_queryset(queryset, keyword) + return self.get_paginated_response_from_queryset(queryset) + + +class LabelContentTypeResourceViewSet(JMSModelViewSet): + serializer_class = serializers.ContentTypeResourceSerializer + rbac_perms = { + 'default': 'labels.view_labeledresource', + 'update': 'labels.change_labeledresource', + } + ordering_fields = ('res_type', 'date_created') + + def get_queryset(self): + label_pk = self.kwargs.get('label') + res_type = self.kwargs.get('res_type') + label = get_object_or_404(Label, pk=label_pk) + content_type = get_object_or_404(ContentType, id=res_type) + bound = self.request.query_params.get('bound', '1') + res_ids = LabeledResource.objects \ + .filter(res_type=content_type, label=label) \ + .values_list('res_id', flat=True) + res_ids = set(res_ids) + model = content_type.model_class() + if hasattr(model, 'get_queryset'): + queryset = model.get_queryset() + else: + queryset = model.objects.all() + if bound == '1': + queryset = queryset.filter(id__in=list(res_ids)) + elif bound == '0': + queryset = queryset.exclude(id__in=list(res_ids)) + keyword = self.request.query_params.get('search') + if keyword: + queryset = content_type.filter_queryset(queryset, keyword) + return queryset + + def put(self, request, *args, **kwargs): + label_pk = self.kwargs.get('label') + res_type = self.kwargs.get('res_type') + content_type = get_object_or_404(ContentType, id=res_type) + label = get_object_or_404(Label, pk=label_pk) + res_ids = request.data.get('res_ids', []) + + LabeledResource.objects \ + .filter(res_type=content_type, label=label) \ + .exclude(res_id__in=res_ids).delete() + resources = [] + for res_id in res_ids: + resources.append(LabeledResource(res_type=content_type, res_id=res_id, label=label, org_id=current_org.id)) + LabeledResource.objects.bulk_create(resources, ignore_conflicts=True) + return Response({"total": len(res_ids)}) + + +class LabelViewSet(OrgBulkModelViewSet): + model = Label + filterset_fields = ("name", "value") + search_fields = filterset_fields + serializer_classes = { + 'default': serializers.LabelSerializer, + 'resource_types': ContentTypeSerializer, + } + rbac_perms = { + 'resource_types': 'labels.view_label', + 'keys': 'labels.view_label', + } + + @action(methods=['GET'], detail=False) + def keys(self, request, *args, **kwargs): + queryset = Label.objects.all() + keyword = request.query_params.get('search') + if keyword: + queryset = queryset.filter(name__icontains=keyword) + keys = queryset.values_list('name', flat=True).distinct() + return Response(keys) + + +class LabeledResourceViewSet(OrgBulkModelViewSet): + model = LabeledResource + filterset_fields = ("label__name", "label__value", "res_type", "res_id", "label") + search_fields = [] + serializer_classes = { + 'default': serializers.LabeledResourceSerializer, + } + ordering_fields = ('res_type', 'date_created') + + # Todo: 这里需要优化,查询 sql 太多 + def filter_search(self, queryset): + keyword = self.request.query_params.get('search') + if not keyword: + return queryset + matched = [] + for instance in queryset: + if keyword.lower() in str(instance.resource).lower(): + matched.append(instance.id) + return queryset.filter(id__in=matched) + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.order_by('res_type') + return queryset + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = self.filter_search(queryset) + return queryset diff --git a/apps/labels/apps.py b/apps/labels/apps.py new file mode 100644 index 000000000..a3bf4dabd --- /dev/null +++ b/apps/labels/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class LabelsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'labels' + verbose_name = _('Labels') diff --git a/apps/labels/const.py b/apps/labels/const.py new file mode 100644 index 000000000..108b07df0 --- /dev/null +++ b/apps/labels/const.py @@ -0,0 +1,25 @@ +from django.utils.functional import LazyObject + + +class LabeledResourceType(LazyObject): + @staticmethod + def get_res_types(): + from rbac.models import ContentType + from .mixins import LabeledMixin + content_types = ContentType.objects.all() + ids = [] + for ct in content_types: + model_cls = ct.model_class() + if not model_cls: + continue + if model_cls._meta.parents: + continue + if issubclass(model_cls, LabeledMixin): + ids.append(ct.id) + return ContentType.objects.filter(id__in=ids) + + def _setup(self): + self._wrapped = self.get_res_types() + + +label_resource_types = LabeledResourceType() diff --git a/apps/labels/migrations/0001_initial.py b/apps/labels/migrations/0001_initial.py new file mode 100644 index 000000000..cefe74e5c --- /dev/null +++ b/apps/labels/migrations/0001_initial.py @@ -0,0 +1,54 @@ +# Generated by Django 4.1.10 on 2023-11-06 10:38 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ] + + operations = [ + migrations.CreateModel( + name='Label', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('internal', models.BooleanField(default=False, verbose_name='Internal')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(db_index=True, max_length=64, verbose_name='Name')), + ('value', models.CharField(max_length=64, verbose_name='Value')), + ], + options={ + 'verbose_name': 'Label', + 'unique_together': {('name', 'value', 'org_id')}, + }, + ), + migrations.CreateModel( + name='LabeledResource', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('res_id', models.CharField(db_index=True, max_length=36, verbose_name='Resource ID')), + ('label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labels.label')), + ('res_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/apps/labels/migrations/0002_auto_20231103_1659.py b/apps/labels/migrations/0002_auto_20231103_1659.py new file mode 100644 index 000000000..49386ed39 --- /dev/null +++ b/apps/labels/migrations/0002_auto_20231103_1659.py @@ -0,0 +1,52 @@ +# Generated by Django 4.1.10 on 2023-11-03 08:59 + +from django.db import migrations + + +def migrate_assets_labels(apps, schema_editor): + old_label_model = apps.get_model('assets', 'Label') + new_label_model = apps.get_model('labels', 'Label') + asset_model = apps.get_model('assets', 'Asset') + labeled_item_model = apps.get_model('labels', 'LabeledResource') + + old_labels = old_label_model.objects.all() + new_labels = [] + old_new_label_map = {} + for label in old_labels: + new_label = new_label_model(name=label.name, value=label.value, org_id=label.org_id) + old_new_label_map[label.id] = new_label + new_labels.append(new_label) + new_label_model.objects.bulk_create(new_labels, ignore_conflicts=True) + + label_relations = asset_model.labels.through.objects.all() + bulk_size = 1000 + count = 0 + content_type = apps.get_model('contenttypes', 'contenttype').objects.get_for_model(asset_model) + + while True: + relations = label_relations[count:count + bulk_size] + if not relations: + break + count += bulk_size + + tagged_items = [] + for relation in relations: + new_label = old_new_label_map[relation.label_id] + tagged_item = labeled_item_model( + label_id=new_label.id, res_type=content_type, + res_id=relation.asset_id, org_id=new_label.org_id + ) + tagged_items.append(tagged_item) + labeled_item_model.objects.bulk_create(tagged_items, ignore_conflicts=True) + + +class Migration(migrations.Migration): + + dependencies = [ + ('labels', '0001_initial'), + ('assets', '0125_auto_20231011_1053') + ] + + operations = [ + migrations.RunPython(migrate_assets_labels), + ] diff --git a/apps/labels/migrations/0003_alter_labeledresource_options_and_more.py b/apps/labels/migrations/0003_alter_labeledresource_options_and_more.py new file mode 100644 index 000000000..c879b7888 --- /dev/null +++ b/apps/labels/migrations/0003_alter_labeledresource_options_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.10 on 2023-11-15 10:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('labels', '0002_auto_20231103_1659'), + ] + + operations = [ + migrations.AlterModelOptions( + name='labeledresource', + options={'verbose_name': 'Labeled resource'}, + ), + migrations.AlterField( + model_name='labeledresource', + name='label', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='labeled_resources', to='labels.label', verbose_name='Label'), + ), + migrations.AlterUniqueTogether( + name='labeledresource', + unique_together={('label', 'res_type', 'res_id', 'org_id')}, + ), + ] diff --git a/apps/labels/migrations/__init__.py b/apps/labels/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/labels/mixins.py b/apps/labels/mixins.py new file mode 100644 index 000000000..90b76f3ed --- /dev/null +++ b/apps/labels/mixins.py @@ -0,0 +1,21 @@ +from django.contrib.contenttypes.fields import GenericRelation +from django.db import models + +from .models import LabeledResource + +__all__ = ['LabeledMixin'] + + +class LabeledMixin(models.Model): + _labels = GenericRelation(LabeledResource, object_id_field='res_id', content_type_field='res_type') + + class Meta: + abstract = True + + @property + def labels(self): + return self._labels + + @labels.setter + def labels(self, value): + self._labels.set(value, bulk=False) diff --git a/apps/labels/models.py b/apps/labels/models.py new file mode 100644 index 000000000..3bf9213f5 --- /dev/null +++ b/apps/labels/models.py @@ -0,0 +1,44 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.utils import lazyproperty +from orgs.mixins.models import JMSOrgBaseModel + + +class Label(JMSOrgBaseModel): + name = models.CharField(max_length=64, verbose_name=_("Name"), db_index=True) + value = models.CharField(max_length=64, unique=False, verbose_name=_("Value")) + internal = models.BooleanField(default=False, verbose_name=_("Internal")) + + class Meta: + unique_together = [('name', 'value', 'org_id')] + verbose_name = _('Label') + + @lazyproperty + def res_count(self): + return self.labeled_resources.count() + + @lazyproperty + def display_name(self): + return '{}:{}'.format(self.name, self.value) + + def __str__(self): + return '{}:{}'.format(self.name, self.value) + + +class LabeledResource(JMSOrgBaseModel): + label = models.ForeignKey( + Label, on_delete=models.CASCADE, related_name='labeled_resources', verbose_name=_("Label") + ) + res_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + res_id = models.CharField(max_length=36, verbose_name=_("Resource ID"), db_index=True) + resource = GenericForeignKey('res_type', 'res_id') + + class Meta: + unique_together = [('label', 'res_type', 'res_id', 'org_id')] + verbose_name = _('Labeled resource') + + def __str__(self): + return '{} => {}'.format(self.label, self.resource) diff --git a/apps/labels/serializers.py b/apps/labels/serializers.py new file mode 100644 index 000000000..803843ad8 --- /dev/null +++ b/apps/labels/serializers.py @@ -0,0 +1,77 @@ +from django.db.models import Count +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from .const import label_resource_types +from .models import Label, LabeledResource + +__all__ = ['LabelSerializer', 'LabeledResourceSerializer', 'ContentTypeResourceSerializer'] + + +class LabelSerializer(BulkOrgResourceModelSerializer): + class Meta: + model = Label + fields = [ + 'id', 'name', 'value', 'res_count', 'comment', + 'date_created', 'date_updated' + ] + read_only_fields = ('date_created', 'date_updated', 'res_count') + extra_kwargs = { + 'res_count': {'label': _('Resource count')}, + } + + @staticmethod + def validate_name(value): + if ':' in value or ',' in value: + raise serializers.ValidationError(_('Cannot contain ":,"')) + return value + + def validate_value(self, value): + return self.validate_name(value) + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.annotate(res_count=Count('labeled_resources')) + return queryset + + +class LabeledResourceSerializer(serializers.ModelSerializer): + res_type = LabeledChoiceField( + choices=[], label=_("Resource type"), source='res_type_id', required=False + ) + label = ObjectRelatedField(queryset=Label.objects, attrs=('id', 'display_name'), label=_("Label")) + resource = serializers.CharField(label=_("Resource")) + + class Meta: + model = LabeledResource + fields = ('id', 'label', 'res_type', 'res_id', 'date_created', 'resource', 'date_updated') + read_only_fields = ('date_created', 'date_updated', 'resource') + + @classmethod + def setup_eager_loading(cls, queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.select_related('label', 'res_type') + return queryset + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_res_type_choices() + + def set_res_type_choices(self): + res_type_field = self.fields.get('res_type') + if not res_type_field: + return + + res_type_field.choices = [(ct.id, ct.name) for ct in label_resource_types] + + +class ContentTypeResourceSerializer(serializers.Serializer): + id = serializers.CharField() + name = serializers.SerializerMethodField() + + @staticmethod + def get_name(obj): + return str(obj) diff --git a/apps/labels/tests.py b/apps/labels/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/labels/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/labels/urls.py b/apps/labels/urls.py new file mode 100644 index 000000000..a3971917f --- /dev/null +++ b/apps/labels/urls.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +from rest_framework_bulk.routes import BulkRouter + +from . import api + +app_name = 'labels' + +router = BulkRouter() +router.register(r'labels', api.LabelViewSet, 'label') +router.register(r'labels/(?P
" +#: labels/models.py:36 +msgid "Resource ID" +msgstr "リソースID" + +#: labels/models.py:41 +msgid "Labeled resource" +msgstr "" + +#: labels/serializers.py:22 +msgid "Resource count" +msgstr "リソース数" + +#: labels/serializers.py:28 +msgid "Cannot contain \":,\"" +msgstr "\":,\"を含めることはできません" + +#: labels/serializers.py:43 +msgid "Resource type" +msgstr "リソースタイプ" + #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "サイトメッセージ" @@ -3932,30 +4032,42 @@ msgstr "システムメッセージ" msgid "Publish the station message" msgstr "投稿サイトニュース" -#: ops/ansible/inventory.py:95 ops/models/job.py:61 +#: ops/ansible/inventory.py:97 ops/models/job.py:62 msgid "No account available" msgstr "利用可能なアカウントがありません" -#: ops/ansible/inventory.py:259 +#: ops/ansible/inventory.py:264 msgid "Ansible disabled" msgstr "Ansible 無効" -#: ops/ansible/inventory.py:275 +#: ops/ansible/inventory.py:280 msgid "Skip hosts below:" msgstr "次のホストをスキップします: " -#: ops/api/celery.py:64 ops/api/celery.py:79 +#: ops/api/celery.py:65 ops/api/celery.py:80 msgid "Waiting task start" msgstr "タスク開始待ち" -#: ops/api/celery.py:162 +#: ops/api/celery.py:203 msgid "Task {} not found" msgstr "タスクは存在しません" -#: ops/api/celery.py:167 +#: ops/api/celery.py:208 msgid "Task {} args or kwargs error" msgstr "タスク実行パラメータエラー" +#: ops/api/job.py:132 +msgid "Duplicate file exists" +msgstr "重複したファイルが存在する" + +#: ops/api/job.py:137 +#, python-brace-format +msgid "" +"File size exceeds maximum limit. Please select a file smaller than {limit}MB" +msgstr "" +"ファイルサイズが最大制限を超えています。{limit}MB より小さいファイルを選択し" +"てください。" + #: ops/api/playbook.py:39 msgid "Currently playbook is being used in a job" msgstr "現在プレイブックは1つのジョブで使用されています" @@ -3984,7 +4096,7 @@ msgstr "ファイルキーこのフィールドは必須です" msgid "This file can not be delete" msgstr "このファイルを削除できません" -#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:57 msgid "App ops" msgstr "アプリ操作" @@ -4024,51 +4136,55 @@ msgstr "VCS" msgid "Adhoc" msgstr "コマンド#コマンド#" -#: ops/const.py:39 ops/models/job.py:138 +#: ops/const.py:39 ops/models/job.py:143 msgid "Playbook" msgstr "Playbook" -#: ops/const.py:43 +#: ops/const.py:40 +msgid "Upload File" +msgstr "ファイルのアップロード" + +#: ops/const.py:44 msgid "Privileged Only" msgstr "特権アカウントのみ" -#: ops/const.py:44 +#: ops/const.py:45 msgid "Privileged First" msgstr "特権アカウント優先" -#: ops/const.py:50 ops/const.py:60 +#: ops/const.py:51 ops/const.py:61 msgid "Powershell" msgstr "PowerShell" -#: ops/const.py:51 ops/const.py:61 +#: ops/const.py:52 ops/const.py:62 msgid "Python" msgstr "Python" -#: ops/const.py:52 ops/const.py:62 +#: ops/const.py:53 ops/const.py:63 msgid "MySQL" msgstr "MySQL" -#: ops/const.py:53 ops/const.py:64 +#: ops/const.py:54 ops/const.py:65 msgid "PostgreSQL" msgstr "PostgreSQL" -#: ops/const.py:54 ops/const.py:65 +#: ops/const.py:55 ops/const.py:66 msgid "SQLServer" msgstr "SQLServer" -#: ops/const.py:55 ops/const.py:67 +#: ops/const.py:56 ops/const.py:68 msgid "Raw" msgstr "" -#: ops/const.py:63 +#: ops/const.py:64 msgid "MariaDB" msgstr "MariaDB" -#: ops/const.py:66 +#: ops/const.py:67 msgid "Oracle" msgstr "Oracle" -#: ops/const.py:73 +#: ops/const.py:74 msgid "Timeout" msgstr "タイムアウト" @@ -4105,15 +4221,17 @@ msgstr "定期的または定期的に設定を行う必要があります" msgid "Pattern" msgstr "パターン" -#: ops/models/adhoc.py:23 ops/models/job.py:133 +#: ops/models/adhoc.py:23 ops/models/job.py:140 msgid "Module" msgstr "モジュール" -#: ops/models/adhoc.py:24 ops/models/celery.py:58 ops/models/job.py:131 +#: ops/models/adhoc.py:24 ops/models/celery.py:58 ops/models/job.py:138 #: terminal/models/component/task.py:14 msgid "Args" msgstr "アルグ" +# msgid "Creator" +# msgstr "作成者" #: ops/models/base.py:19 msgid "Account policy" msgstr "アカウント ポリシー" @@ -4122,16 +4240,16 @@ msgstr "アカウント ポリシー" msgid "Last execution" msgstr "最後の実行" -#: ops/models/base.py:22 ops/serializers/job.py:19 +#: ops/models/base.py:22 ops/serializers/job.py:17 msgid "Date last run" msgstr "最終実行日" -#: ops/models/base.py:51 ops/models/job.py:227 -#: xpack/plugins/cloud/models.py:199 +#: ops/models/base.py:51 ops/models/job.py:231 +#: xpack/plugins/cloud/models.py:202 msgid "Result" msgstr "結果" -#: ops/models/base.py:52 ops/models/job.py:228 +#: ops/models/base.py:52 ops/models/job.py:232 msgid "Summary" msgstr "概要" @@ -4164,43 +4282,43 @@ msgstr "発売日" msgid "Celery Task Execution" msgstr "Celery タスク実行" -#: ops/models/job.py:135 +#: ops/models/job.py:141 msgid "Chdir" msgstr "実行ディレクトリ" -#: ops/models/job.py:136 +#: ops/models/job.py:142 msgid "Timeout (Seconds)" msgstr "タイムアウト(秒)" -#: ops/models/job.py:143 +#: ops/models/job.py:147 msgid "Use Parameter Define" msgstr "パラメータ定義を使用する" -#: ops/models/job.py:144 +#: ops/models/job.py:148 msgid "Parameters define" msgstr "パラメータ定義" -#: ops/models/job.py:145 +#: ops/models/job.py:149 msgid "Runas" msgstr "ユーザーとして実行" -#: ops/models/job.py:147 +#: ops/models/job.py:151 msgid "Runas policy" msgstr "ユーザー ポリシー" -#: ops/models/job.py:211 +#: ops/models/job.py:215 msgid "Job" msgstr "ジョブ#ジョブ#" -#: ops/models/job.py:234 +#: ops/models/job.py:238 msgid "Material" msgstr "Material" -#: ops/models/job.py:236 +#: ops/models/job.py:240 msgid "Material Type" msgstr "Material を選択してオプションを設定します。" -#: ops/models/job.py:536 +#: ops/models/job.py:557 msgid "Job Execution" msgstr "ジョブ実行" @@ -4240,19 +4358,19 @@ msgstr "{max_threshold}%: => {value} を超える使用メモリ" msgid "CPU load more than {max_threshold}: => {value}" msgstr "{max_threshold} を超えるCPUロード: => {value}" -#: ops/serializers/job.py:17 +#: ops/serializers/job.py:15 msgid "Run after save" msgstr "保存後に実行" -#: ops/serializers/job.py:54 +#: ops/serializers/job.py:62 msgid "Job type" msgstr "タスクの種類" -#: ops/serializers/job.py:57 terminal/serializers/session.py:53 +#: ops/serializers/job.py:65 terminal/serializers/session.py:53 msgid "Is finished" msgstr "終了しました" -#: ops/serializers/job.py:58 +#: ops/serializers/job.py:66 msgid "Time cost" msgstr "時を過ごす" @@ -4343,7 +4461,7 @@ msgstr "" msgid "The organization have resource ({}) cannot be deleted" msgstr "組織のリソース ({}) は削除できません" -#: orgs/apps.py:7 rbac/tree.py:126 +#: orgs/apps.py:7 rbac/tree.py:128 msgid "App organizations" msgstr "アプリ組織" @@ -4372,7 +4490,8 @@ msgstr "デフォルト組織" msgid "SYSTEM" msgstr "システム組織" -#: orgs/models.py:83 rbac/models/role.py:36 terminal/models/applet/applet.py:41 +#: orgs/models.py:83 rbac/models/role.py:36 settings/models.py:183 +#: terminal/models/applet/applet.py:41 msgid "Builtin" msgstr "ビルトイン" @@ -4432,7 +4551,7 @@ msgstr "転送" msgid "Clipboard" msgstr "クリップボード" -#: perms/models/asset_permission.py:88 +#: perms/models/asset_permission.py:89 msgid "Asset permission" msgstr "資産権限" @@ -4468,6 +4587,11 @@ msgstr "認定アカウント" msgid "today" msgstr "今日" +#: perms/notifications.py:12 perms/notifications.py:44 +#: settings/serializers/feature.py:106 +msgid "day" +msgstr "日" + #: perms/notifications.py:15 msgid "You permed assets is about to expire" msgstr "パーマ資産の有効期限が近づいています" @@ -4497,11 +4621,11 @@ msgstr "アセット許可の有効期限通知を送信する" #, python-format msgid "" "\n" -" The following %(item_type)s will expire in %(count)s days\n" +" The following %(item_type)s will expire in %(count)s\n" " " msgstr "" "\n" -" 次の %(item_type)s は %(count)s 日以内に期限切れになります\n" +" 次の %(item_type)s は %(count)s 以内に期限切れになります\n" " " #: rbac/api/role.py:35 @@ -4512,7 +4636,7 @@ msgstr "内部の役割は、破壊することはできません" msgid "The role has been bound to users, can't be destroy" msgstr "ロールはユーザーにバインドされており、破壊することはできません" -#: rbac/api/role.py:87 +#: rbac/api/role.py:100 msgid "Internal role, can't be update" msgstr "内部ロール、更新できません" @@ -4576,7 +4700,7 @@ msgstr "ファイルマネージャを表示できます" msgid "Can view System Tools" msgstr "システムツールを表示できます" -#: rbac/models/permission.py:27 rbac/models/role.py:34 +#: rbac/models/permission.py:78 rbac/models/role.py:34 msgid "Permissions" msgstr "権限" @@ -4586,7 +4710,7 @@ msgid "Scope" msgstr "スコープ" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:803 +#: users/models/user.py:810 msgid "Role" msgstr "ロール" @@ -4625,11 +4749,12 @@ msgstr "システムロールバインディング" msgid "Perms" msgstr "パーマ" -#: rbac/serializers/role.py:27 users/serializers/group.py:30 +#: rbac/serializers/role.py:27 users/serializers/group.py:31 msgid "Users amount" msgstr "ユーザー数" #: rbac/serializers/role.py:28 terminal/models/applet/applet.py:34 +#: terminal/models/virtualapp/virtualapp.py:20 msgid "Display name" msgstr "表示名" @@ -4653,7 +4778,7 @@ msgstr "ワークスペースビュー" msgid "Audit view" msgstr "監査ビュー" -#: rbac/tree.py:27 settings/models.py:158 +#: rbac/tree.py:27 settings/models.py:159 msgid "System setting" msgstr "システム設定" @@ -4661,59 +4786,64 @@ msgstr "システム設定" msgid "Session audits" msgstr "セッション監査" -#: rbac/tree.py:47 +#: rbac/tree.py:49 msgid "Cloud import" msgstr "クラウドインポート" -#: rbac/tree.py:48 +#: rbac/tree.py:50 msgid "Backup account" msgstr "バックアップアカウント" -#: rbac/tree.py:49 +#: rbac/tree.py:51 msgid "Gather account" msgstr "アカウントを集める" -#: rbac/tree.py:51 +#: rbac/tree.py:53 msgid "Asset change auth" msgstr "資産の改ざん" -#: rbac/tree.py:52 +#: rbac/tree.py:54 msgid "Terminal setting" msgstr "ターミナル設定" -#: rbac/tree.py:53 +#: rbac/tree.py:55 msgid "Task Center" msgstr "タスクセンター" -#: rbac/tree.py:54 +#: rbac/tree.py:56 msgid "My assets" msgstr "私の資産" -#: rbac/tree.py:56 terminal/models/applet/applet.py:52 -#: terminal/models/applet/applet.py:315 terminal/models/applet/host.py:30 +#: rbac/tree.py:58 terminal/models/applet/applet.py:52 +#: terminal/models/applet/applet.py:316 terminal/models/applet/host.py:30 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "リモートアプリケーション" -#: rbac/tree.py:127 +#: rbac/tree.py:129 msgid "Ticket comment" msgstr "チケットコメント" -#: rbac/tree.py:128 settings/serializers/feature.py:58 +#: rbac/tree.py:130 settings/serializers/feature.py:98 #: tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "チケット" -#: rbac/tree.py:129 +#: rbac/tree.py:131 msgid "Common setting" msgstr "共通設定" -#: rbac/tree.py:130 +#: rbac/tree.py:132 msgid "View permission tree" msgstr "権限ツリーの表示" -#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 -#: settings/api/sms.py:160 settings/api/vault.py:40 settings/api/wecom.py:37 +#: settings/api/chat.py:40 +msgid "Chat AI is not enabled" +msgstr "チャットAIがオンになっていない" + +#: settings/api/chat.py:79 settings/api/dingtalk.py:31 +#: settings/api/feishu.py:36 settings/api/slack.py:34 settings/api/sms.py:160 +#: settings/api/vault.py:40 settings/api/wecom.py:37 msgid "Test success" msgstr "テストの成功" @@ -4721,23 +4851,18 @@ msgstr "テストの成功" msgid "Test mail sent to {}, please check" msgstr "{}に送信されたテストメールを確認してください" -#: settings/api/ldap.py:176 -msgid "Synchronization start, please wait." -msgstr "同期開始、お待ちください。" +#: settings/api/ldap.py:101 +msgid "" +"Users are not synchronized, please click the user synchronization button" +msgstr "" +"ユーザーは同期されていません。「ユーザーを同期」ボタンをクリックしてくださ" +"い。" -#: settings/api/ldap.py:180 -msgid "Synchronization is running, please wait." -msgstr "同期が実行中です。しばらくお待ちください。" - -#: settings/api/ldap.py:185 -msgid "Synchronization error: {}" -msgstr "同期エラー: {}" - -#: settings/api/ldap.py:223 +#: settings/api/ldap.py:137 msgid "Get ldap users is None" msgstr "Ldapユーザーを取得するにはNone" -#: settings/api/ldap.py:233 +#: settings/api/ldap.py:147 msgid "Imported {} users successfully (Organization: {})" msgstr "{} 人のユーザーを正常にインポートしました (組織: {})" @@ -4753,66 +4878,78 @@ msgstr "携帯番号をテストこのフィールドは必須です" msgid "Settings" msgstr "設定" -#: settings/models.py:35 users/models/preference.py:14 +#: settings/models.py:36 users/models/preference.py:14 msgid "Encrypted" msgstr "暗号化された" -#: settings/models.py:160 +#: settings/models.py:161 msgid "Can change email setting" msgstr "メール設定を変更できます" -#: settings/models.py:161 +#: settings/models.py:162 msgid "Can change auth setting" msgstr "資格認定の設定" -#: settings/models.py:162 +#: settings/models.py:163 msgid "Can change auth ops" msgstr "タスクセンターの設定" -#: settings/models.py:163 +#: settings/models.py:164 msgid "Can change auth ticket" msgstr "製造オーダ設定" -#: settings/models.py:164 +#: settings/models.py:165 +msgid "Can change virtual app setting" +msgstr "仮想アプリケーション設定を変更できます" + +#: settings/models.py:166 msgid "Can change auth announcement" msgstr "公告の設定" -#: settings/models.py:165 +#: settings/models.py:167 msgid "Can change vault setting" msgstr "金庫の設定を変えることができます" -#: settings/models.py:166 +#: settings/models.py:168 +msgid "Can change chat ai setting" +msgstr "チャットAI設定を変更できます" + +#: settings/models.py:169 msgid "Can change system msg sub setting" msgstr "システムmsgサブ设定を変更できます" -#: settings/models.py:167 +#: settings/models.py:170 msgid "Can change sms setting" msgstr "Smsの設定を変えることができます" -#: settings/models.py:168 +#: settings/models.py:171 msgid "Can change security setting" msgstr "セキュリティ設定を変更できます" -#: settings/models.py:169 +#: settings/models.py:172 msgid "Can change clean setting" msgstr "きれいな設定を変えることができます" -#: settings/models.py:170 +#: settings/models.py:173 msgid "Can change interface setting" msgstr "インターフェイスの設定を変えることができます" -#: settings/models.py:171 +#: settings/models.py:174 msgid "Can change license setting" msgstr "ライセンス設定を変更できます" -#: settings/models.py:172 +#: settings/models.py:175 msgid "Can change terminal setting" msgstr "ターミナルの設定を変えることができます" -#: settings/models.py:173 +#: settings/models.py:176 msgid "Can change other setting" msgstr "他の設定を変えることができます" +#: settings/models.py:186 +msgid "Chat prompt" +msgstr "チャットのヒント" + #: settings/serializers/auth/base.py:12 msgid "LDAP Auth" msgstr "LDAP 認証" @@ -4846,22 +4983,26 @@ msgid "FeiShu Auth" msgstr "飛本 認証" #: settings/serializers/auth/base.py:20 +msgid "Slack Auth" +msgstr "Slack 認証" + +#: settings/serializers/auth/base.py:21 msgid "WeCom Auth" msgstr "企業微信 認証" -#: settings/serializers/auth/base.py:21 +#: settings/serializers/auth/base.py:22 msgid "SSO Auth" msgstr "SSO Token 認証" -#: settings/serializers/auth/base.py:22 +#: settings/serializers/auth/base.py:23 msgid "Passkey Auth" msgstr "Passkey 認証" -#: settings/serializers/auth/base.py:25 +#: settings/serializers/auth/base.py:26 msgid "Forgot password url" msgstr "パスワードのURLを忘れた" -#: settings/serializers/auth/base.py:28 +#: settings/serializers/auth/base.py:29 msgid "Enable login redirect msg" msgstr "ログインリダイレクトの有効化msg" @@ -5163,6 +5304,10 @@ msgstr "SP プライベートキー" msgid "SP cert" msgstr "SP 証明書" +#: settings/serializers/auth/slack.py:12 +msgid "Enable Slack Auth" +msgstr "Slack 認証の有効化" + #: settings/serializers/auth/sms.py:17 msgid "Enable SMS" msgstr "SMSの有効化" @@ -5242,11 +5387,6 @@ msgstr "URL" msgid "Request method" msgstr "請求方法です" -#: settings/serializers/auth/sms.py:117 -#, python-format -msgid "The value in the parameter must contain %s" -msgstr "パラメータの値には必ず %s が含まれます" - #: settings/serializers/auth/sso.py:16 msgid "Enable SSO auth" msgstr "SSO Token認証の有効化" @@ -5261,7 +5401,7 @@ msgid "SSO auth key TTL" msgstr "Token有効期間" #: settings/serializers/auth/sso.py:20 -#: xpack/plugins/cloud/serializers/account_attrs.py:193 +#: xpack/plugins/cloud/serializers/account_attrs.py:200 msgid "Unit: second" msgstr "単位: 秒" @@ -5269,11 +5409,11 @@ msgstr "単位: 秒" msgid "Enable WeCom Auth" msgstr "企業微信認証の有効化" -#: settings/serializers/basic.py:9 +#: settings/serializers/basic.py:11 msgid "Site url" msgstr "サイトURL" -#: settings/serializers/basic.py:11 +#: settings/serializers/basic.py:13 msgid "" "External URL, email links or other system callbacks are used to access it, " "eg: http://dev.jumpserver.org:8080" @@ -5281,38 +5421,42 @@ msgstr "" "外部URL、メールリンクまたは他のシステムコールバックにアクセスするには、" "http://dev.jumpserver.org:8080などを使用します" -#: settings/serializers/basic.py:16 +#: settings/serializers/basic.py:18 msgid "User guide url" msgstr "ユーザーガイドurl" -#: settings/serializers/basic.py:17 +#: settings/serializers/basic.py:19 msgid "User first login update profile done redirect to it" msgstr "ユーザーの最初のログイン更新プロファイルがリダイレクトされました" -#: settings/serializers/basic.py:20 +#: settings/serializers/basic.py:22 msgid "Global organization name" msgstr "グローバル組織名" -#: settings/serializers/basic.py:21 +#: settings/serializers/basic.py:23 msgid "The name of global organization to display" msgstr "表示するグローバル組織の名前" -#: settings/serializers/basic.py:24 +#: settings/serializers/basic.py:26 msgid "Help Docs URL" msgstr "ドキュメントリンク" -#: settings/serializers/basic.py:25 +#: settings/serializers/basic.py:27 msgid "default: http://docs.jumpserver.org" msgstr "デフォルト: http://docs.jumpserver.org" -#: settings/serializers/basic.py:28 +#: settings/serializers/basic.py:30 msgid "Help Support URL" msgstr "サポートリンク" -#: settings/serializers/basic.py:29 +#: settings/serializers/basic.py:31 msgid "default: http://www.jumpserver.org/support/" msgstr "デフォルト: http://www.jumpserver.org/support/" +#: settings/serializers/basic.py:44 +msgid "Organization name already exists" +msgstr "組織名はすでに存在します。" + #: settings/serializers/cleaning.py:11 msgid "Period clean" msgstr "定時清掃" @@ -5357,70 +5501,98 @@ msgstr "" "この期間を超えるセッション、録音、およびコマンド レコードは削除されます (デー" "タベースのバックアップに影響し、OSS などには影響しません)" -#: settings/serializers/feature.py:16 +#: settings/serializers/feature.py:18 msgid "Subject" msgstr "件名" -#: settings/serializers/feature.py:20 +#: settings/serializers/feature.py:22 msgid "More url" msgstr "もっとURL" -#: settings/serializers/feature.py:34 settings/serializers/feature.py:37 +#: settings/serializers/feature.py:36 settings/serializers/feature.py:39 msgid "Announcement" msgstr "発表" -#: settings/serializers/feature.py:36 +#: settings/serializers/feature.py:38 msgid "Enable announcement" msgstr "アナウンスの有効化" -#: settings/serializers/feature.py:44 +#: settings/serializers/feature.py:46 msgid "Enable Vault" msgstr "有効化 Vault" -#: settings/serializers/feature.py:53 +#: settings/serializers/feature.py:55 msgid "Mount Point" msgstr "マウントポイント" #: settings/serializers/feature.py:60 +msgid "Chat AI" +msgstr "チャットAI" + +#: settings/serializers/feature.py:64 +msgid "Enable Chat AI" +msgstr "チャットAIを起動する" + +#: settings/serializers/feature.py:67 +msgid "Base Url" +msgstr "基本的なUrl" + +#: settings/serializers/feature.py:70 templates/_header_bar.html:90 +msgid "API Key" +msgstr "API Key" + +#: settings/serializers/feature.py:76 +msgid "GPT Model" +msgstr "GPTモデル" + +#: settings/serializers/feature.py:100 msgid "Enable tickets" msgstr "チケットを有効にする" -#: settings/serializers/feature.py:63 +#: settings/serializers/feature.py:103 msgid "Ticket authorize default time" msgstr "デフォルト製造オーダ承認時間" -#: settings/serializers/feature.py:66 -msgid "day" -msgstr "日" - -#: settings/serializers/feature.py:66 +#: settings/serializers/feature.py:106 msgid "hour" msgstr "時" -#: settings/serializers/feature.py:67 +#: settings/serializers/feature.py:107 msgid "Ticket authorize default time unit" msgstr "デフォルト製造オーダ承認時間単位" -#: settings/serializers/feature.py:72 +#: settings/serializers/feature.py:112 msgid "Feature" msgstr "機能" -#: settings/serializers/feature.py:75 +#: settings/serializers/feature.py:115 msgid "Operation center" msgstr "職業センター" -#: settings/serializers/feature.py:76 +#: settings/serializers/feature.py:116 msgid "Allow user run batch command or not using ansible" msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" -#: settings/serializers/feature.py:80 +#: settings/serializers/feature.py:120 msgid "Operation center command blacklist" msgstr "オペレーション センター コマンド ブラックリスト" -#: settings/serializers/feature.py:81 +#: settings/serializers/feature.py:121 msgid "Commands that are not allowed execute." msgstr "実行が許可されていないコマンド" +#: settings/serializers/feature.py:126 +#: terminal/models/virtualapp/provider.py:17 +#: terminal/models/virtualapp/virtualapp.py:36 +#: terminal/models/virtualapp/virtualapp.py:97 +#: terminal/serializers/virtualapp.py:32 +msgid "Virtual app" +msgstr "仮想アプリケーション" + +#: settings/serializers/feature.py:129 +msgid "Enable virtual app" +msgstr "仮想アプリケーションの有効化" + #: settings/serializers/msg.py:24 msgid "SMTP host" msgstr "SMTPホスト" @@ -5822,7 +5994,7 @@ msgstr "複数のユーザーを使用して、分割" msgid "[%s] %s" msgstr "[%s] %s" -#: settings/serializers/terminal.py:9 +#: settings/serializers/terminal.py:9 terminal/models/virtualapp/provider.py:11 msgid "Hostname" msgstr "ホスト名" @@ -5886,100 +6058,104 @@ msgstr "LDAP ユーザーを定期的にインポートする" msgid "Registration periodic import ldap user task" msgstr "登録サイクルLDAPユーザータスクのインポート" -#: settings/utils/ldap.py:494 +#: settings/utils/ldap.py:492 msgid "ldap:// or ldaps:// protocol is used." msgstr "ldap:// または ldaps:// プロトコルが使用されます。" -#: settings/utils/ldap.py:505 +#: settings/utils/ldap.py:503 msgid "Host or port is disconnected: {}" msgstr "ホストまたはポートが切断されました: {}" -#: settings/utils/ldap.py:507 +#: settings/utils/ldap.py:505 msgid "The port is not the port of the LDAP service: {}" msgstr "ポートはLDAPサービスのポートではありません: {}" -#: settings/utils/ldap.py:509 +#: settings/utils/ldap.py:507 msgid "Please add certificate: {}" msgstr "証明書を追加してください: {}" -#: settings/utils/ldap.py:513 settings/utils/ldap.py:540 -#: settings/utils/ldap.py:570 settings/utils/ldap.py:598 +#: settings/utils/ldap.py:511 settings/utils/ldap.py:538 +#: settings/utils/ldap.py:568 settings/utils/ldap.py:596 msgid "Unknown error: {}" msgstr "不明なエラー: {}" -#: settings/utils/ldap.py:527 +#: settings/utils/ldap.py:525 msgid "Bind DN or Password incorrect" msgstr "DNまたはパスワードのバインドが正しくありません" -#: settings/utils/ldap.py:534 +#: settings/utils/ldap.py:532 msgid "Please enter Bind DN: {}" msgstr "バインドDN: {} を入力してください" -#: settings/utils/ldap.py:536 +#: settings/utils/ldap.py:534 msgid "Please enter Password: {}" msgstr "パスワードを入力してください: {}" -#: settings/utils/ldap.py:538 +#: settings/utils/ldap.py:536 msgid "Please enter correct Bind DN and Password: {}" msgstr "正しいバインドDNとパスワードを入力してください: {}" -#: settings/utils/ldap.py:556 +#: settings/utils/ldap.py:554 msgid "Invalid User OU or User search filter: {}" msgstr "無効なユーザー OU またはユーザー検索フィルター: {}" -#: settings/utils/ldap.py:587 +#: settings/utils/ldap.py:585 msgid "LDAP User attr map not include: {}" msgstr "LDAP ユーザーattrマップは含まれません: {}" -#: settings/utils/ldap.py:594 +#: settings/utils/ldap.py:592 msgid "LDAP User attr map is not dict" msgstr "LDAPユーザーattrマップはdictではありません" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:611 msgid "LDAP authentication is not enabled" msgstr "LDAP 認証が有効になっていない" -#: settings/utils/ldap.py:631 +#: settings/utils/ldap.py:629 msgid "Error (Invalid LDAP server): {}" msgstr "エラー (LDAPサーバーが無効): {}" -#: settings/utils/ldap.py:633 +#: settings/utils/ldap.py:631 msgid "Error (Invalid Bind DN): {}" msgstr "エラー (DNのバインドが無効): {}" -#: settings/utils/ldap.py:635 +#: settings/utils/ldap.py:633 msgid "Error (Invalid LDAP User attr map): {}" msgstr "エラー (LDAPユーザーattrマップが無効): {}" -#: settings/utils/ldap.py:637 +#: settings/utils/ldap.py:635 msgid "Error (Invalid User OU or User search filter): {}" msgstr "エラー (ユーザーOUまたはユーザー検索フィルターが無効): {}" -#: settings/utils/ldap.py:639 +#: settings/utils/ldap.py:637 msgid "Error (Not enabled LDAP authentication): {}" msgstr "エラー (LDAP認証が有効化されていません): {}" -#: settings/utils/ldap.py:641 +#: settings/utils/ldap.py:639 msgid "Error (Unknown): {}" msgstr "エラー (不明): {}" -#: settings/utils/ldap.py:644 +#: settings/utils/ldap.py:642 msgid "Succeed: Match {} s user" msgstr "成功: {} 人のユーザーに一致" -#: settings/utils/ldap.py:677 +#: settings/utils/ldap.py:653 +msgid "Please test the connection first" +msgstr "まず接続をテストしてください" + +#: settings/utils/ldap.py:675 msgid "Authentication failed (configuration incorrect): {}" msgstr "認証に失敗しました (設定が正しくありません): {}" -#: settings/utils/ldap.py:681 +#: settings/utils/ldap.py:679 msgid "Authentication failed (username or password incorrect): {}" msgstr "認証に失敗しました (ユーザー名またはパスワードが正しくありません): {}" -#: settings/utils/ldap.py:683 +#: settings/utils/ldap.py:681 msgid "Authentication failed (Unknown): {}" msgstr "認証に失敗しました (不明): {}" -#: settings/utils/ldap.py:686 +#: settings/utils/ldap.py:684 msgid "Authentication success: {}" msgstr "認証成功: {}" @@ -6043,10 +6219,6 @@ msgstr "ページの管理" msgid "User page" msgstr "ユーザーページ" -#: templates/_header_bar.html:90 -msgid "API Key" -msgstr "API Key" - #: templates/_header_bar.html:91 msgid "Logout" msgstr "ログアウト" @@ -6144,7 +6316,7 @@ msgid "Home page" msgstr "ホームページ" #: templates/resource_download.html:18 templates/resource_download.html:33 -#: users/const.py:53 +#: users/const.py:60 msgid "Client" msgstr "クライアント" @@ -6189,11 +6361,13 @@ msgstr "" msgid "Offline video player" msgstr "オフラインビデオプレーヤー" -#: terminal/api/applet/applet.py:48 terminal/api/applet/applet.py:51 +#: terminal/api/applet/applet.py:50 terminal/api/applet/applet.py:53 +#: terminal/api/virtualapp/virtualapp.py:43 +#: terminal/api/virtualapp/virtualapp.py:46 msgid "Invalid zip file" msgstr "zip ファイルが無効です" -#: terminal/api/applet/applet.py:65 +#: terminal/api/applet/applet.py:72 msgid "This is enterprise edition applet" msgstr "これはエンタープライズ版アプレットです" @@ -6315,7 +6489,7 @@ msgstr "クリティカル" msgid "High" msgstr "高い" -#: terminal/const.py:47 terminal/const.py:83 +#: terminal/const.py:47 terminal/const.py:84 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" @@ -6324,46 +6498,50 @@ msgstr "正常" msgid "Offline" msgstr "オフライン" -#: terminal/const.py:79 +#: terminal/const.py:80 msgid "Mismatch" msgstr "一致しない" -#: terminal/const.py:84 +#: terminal/const.py:85 msgid "Tunnel" msgstr "ちかチャネル" -#: terminal/const.py:90 +#: terminal/const.py:91 msgid "Read only" msgstr "読み取り専用" -#: terminal/const.py:91 +#: terminal/const.py:92 msgid "Writable" msgstr "書き込み可能" -#: terminal/const.py:95 +#: terminal/const.py:96 msgid "Kill session" msgstr "セッションを終了する" -#: terminal/const.py:96 +#: terminal/const.py:97 msgid "Lock session" msgstr "セッションをロックする" -#: terminal/const.py:97 +#: terminal/const.py:98 msgid "Unlock session" msgstr "セッションのロックを解除する" -#: terminal/const.py:102 +#: terminal/const.py:103 msgid "Replay create failed" msgstr "ビデオの作成に失敗しました" -#: terminal/const.py:103 +#: terminal/const.py:104 msgid "Replay upload failed" msgstr "動画のアップロードに失敗しました" -#: terminal/const.py:104 +#: terminal/const.py:105 msgid "Replay convert failed" msgstr "ビデオのトランスコーディングに失敗しました" +#: terminal/const.py:106 +msgid "Replay unsupported" +msgstr "録画はサポートされていません" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "一括作成非サポート" @@ -6381,6 +6559,7 @@ msgid "Enterprise" msgstr "エンタープライズ版" #: terminal/models/applet/applet.py:36 +#: terminal/models/virtualapp/virtualapp.py:22 msgid "Author" msgstr "著者" @@ -6393,6 +6572,7 @@ msgid "Can concurrent" msgstr "同時実行可能" #: terminal/models/applet/applet.py:44 +#: terminal/models/virtualapp/virtualapp.py:29 msgid "Tags" msgstr "ラベル" @@ -6401,6 +6581,7 @@ msgid "Hosts" msgstr "ホスト" #: terminal/models/applet/applet.py:93 +#: terminal/models/virtualapp/virtualapp.py:66 msgid "Applet pkg not valid, Missing file {}" msgstr "無効なアプレット パッケージ、ファイル {} がありません" @@ -6416,12 +6597,12 @@ msgstr "カスタムプラットフォームのみをサポート" msgid "Missing type in platform.yml" msgstr "platform.ymlにタイプがありません" -#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:36 +#: terminal/models/applet/applet.py:318 terminal/models/applet/host.py:36 #: terminal/models/applet/host.py:138 msgid "Hosting" msgstr "ホスト マシン" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:57 +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:69 msgid "Deploy options" msgstr "展開パラメーター" @@ -6447,7 +6628,7 @@ msgstr "同期日" #: terminal/models/applet/host.py:28 msgid "Using same account" -msgstr "同じ名前のアカウントを使用" +msgstr "同じアカウントを使用する" #: terminal/models/applet/host.py:139 msgid "Initial" @@ -6485,15 +6666,19 @@ msgstr "PostgreSQL ポート" msgid "Redis port" msgstr "Redis ポート" -#: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:102 +#: terminal/models/component/endpoint.py:23 +msgid "SQLServer port" +msgstr "SQLServer ポート" + +#: terminal/models/component/endpoint.py:30 +#: terminal/models/component/endpoint.py:103 #: terminal/serializers/endpoint.py:73 terminal/serializers/storage.py:41 #: terminal/serializers/storage.py:53 terminal/serializers/storage.py:83 #: terminal/serializers/storage.py:93 terminal/serializers/storage.py:101 msgid "Endpoint" msgstr "エンドポイント" -#: terminal/models/component/endpoint.py:108 +#: terminal/models/component/endpoint.py:109 msgid "Endpoint rule" msgstr "エンドポイントルール" @@ -6551,7 +6736,7 @@ msgstr "リモートアドレス" msgid "Application User" msgstr "ユーザーの適用" -#: terminal/models/component/terminal.py:166 +#: terminal/models/component/terminal.py:176 msgid "Can view terminal config" msgstr "ターミナル構成を表示できます" @@ -6673,6 +6858,19 @@ msgstr "検証コードが無効" msgid "You have already joined this session" msgstr "すでにこのセッションに参加しています" +#: terminal/models/virtualapp/virtualapp.py:32 +msgid "Providers" +msgstr "プロバイダ" + +#: terminal/models/virtualapp/virtualapp.py:94 +#: terminal/serializers/virtualapp.py:34 +msgid "App Provider" +msgstr "アプリケーションプロバイダ" + +#: terminal/models/virtualapp/virtualapp.py:102 +msgid "Virtual app publication" +msgstr "仮想アプリケーションの公開" + #: terminal/notifications.py:25 msgid "Sessions" msgstr "セッション" @@ -6710,7 +6908,7 @@ msgstr "テスト失敗: アカウントが無効" msgid "Invalid storage" msgstr "無効なストレージ" -#: terminal/serializers/applet.py:28 +#: terminal/serializers/applet.py:28 terminal/serializers/virtualapp.py:15 msgid "Icon" msgstr "アイコン" @@ -6765,19 +6963,36 @@ msgstr "RDS 認可モード" msgid "RDS Single Session Per User" msgstr "RDS シングル ユーザー シングル セッション" -#: terminal/serializers/applet_host.py:52 -msgid "RDS Max Disconnection Time" -msgstr "最大切断時間" - #: terminal/serializers/applet_host.py:53 -msgid "RDS Remote App Logoff Time Limit" -msgstr "RDS 远程应用注销时间限制" +msgid "RDS Max Disconnection Time (ms)" +msgstr "最大切断時間(ミリ秒)" -#: terminal/serializers/applet_host.py:59 terminal/serializers/terminal.py:47 +#: terminal/serializers/applet_host.py:55 +msgid "" +"Tips: Set the maximum duration for keeping a disconnected session active on " +"the server (log off the session after 60000 milliseconds)." +msgstr "" +"ヒント:サーバー上で切断されたセッションがアクティブな状態で維持される最大時" +"間を設定します(60000ミリ秒後にセッションをログオフ)。" + +#: terminal/serializers/applet_host.py:60 +msgid "RDS Remote App Logoff Time Limit (ms)" +msgstr "RDSリモートアプリケーションのログアウト時間制限(ミリ秒)" + +#: terminal/serializers/applet_host.py:62 +msgid "" +"Tips: Set the logoff time for RemoteApp sessions after closing all RemoteApp " +"programs (0 milliseconds, log off the session immediately)." +msgstr "" +"ヒント:すべてのRemoteAppプログラムを閉じた後、RemoteAppセッションのログオフ" +"時間を設定します(0ミリ秒、セッションを即座にログオフ)。" + +#: terminal/serializers/applet_host.py:71 terminal/serializers/terminal.py:47 +#: terminal/serializers/virtualapp_provider.py:13 msgid "Load status" msgstr "ロードステータス" -#: terminal/serializers/applet_host.py:73 +#: terminal/serializers/applet_host.py:85 msgid "" "These accounts are used to connect to the published application, the account " "is now divided into two types, one is dedicated to each account, each user " @@ -6791,11 +7006,11 @@ msgstr "" "開されています。アプリケーションが複数のオープンをサポートしていない場合、お" "よび特別なものが使用されている場合、公開アカウントが使用されます。" -#: terminal/serializers/applet_host.py:80 +#: terminal/serializers/applet_host.py:92 msgid "The number of public accounts created automatically" msgstr "自動的に作成される公開アカウントの数" -#: terminal/serializers/applet_host.py:83 +#: terminal/serializers/applet_host.py:95 msgid "" "Connect to the host using the same account first. For security reasons, " "please set the configuration item CACHE_LOGIN_PASSWORD_ENABLED=true and " @@ -6926,7 +7141,7 @@ msgstr "アクセスキー" msgid "Access key secret" msgstr "アクセスキーシークレット" -#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:250 +#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:253 msgid "Region" msgstr "リージョン" @@ -6946,8 +7161,8 @@ msgstr "エンドポイントサフィックス" msgid "HOST" msgstr "ホスト" -#: terminal/serializers/storage.py:146 users/models/user.py:823 -#: xpack/plugins/cloud/serializers/account_attrs.py:206 +#: terminal/serializers/storage.py:146 users/models/user.py:830 +#: xpack/plugins/cloud/serializers/account_attrs.py:213 msgid "Private key" msgstr "ssh秘密鍵" @@ -6991,6 +7206,26 @@ msgstr "セッション" msgid "Not found" msgstr "見つかりません" +#: terminal/serializers/virtualapp_provider.py:26 +msgid "Container ID" +msgstr "コンテナID" + +#: terminal/serializers/virtualapp_provider.py:27 +msgid "Container Image" +msgstr "コンテナミラーリング" + +#: terminal/serializers/virtualapp_provider.py:28 +msgid "Container Name" +msgstr "コンテナー名" + +#: terminal/serializers/virtualapp_provider.py:29 +msgid "Container Status" +msgstr "コンテナステータス" + +#: terminal/serializers/virtualapp_provider.py:30 +msgid "Container Ports" +msgstr "コンテナポート" + #: terminal/tasks.py:33 msgid "Periodic delete terminal status" msgstr "端末の状態を定期的にクリーンアップする" @@ -7272,19 +7507,19 @@ msgstr "チケット基本情報" msgid "Ticket applied info" msgstr "チケット適用情報" -#: tickets/notifications.py:109 +#: tickets/notifications.py:111 msgid "Your has a new ticket, applicant - {}" msgstr "新しいチケットがあります- {}" -#: tickets/notifications.py:113 +#: tickets/notifications.py:115 msgid "{}: New Ticket - {} ({})" msgstr "新しいチケット- {} ({})" -#: tickets/notifications.py:157 +#: tickets/notifications.py:159 msgid "Your ticket has been processed, processor - {}" msgstr "チケットが処理されました。プロセッサー- {}" -#: tickets/notifications.py:161 +#: tickets/notifications.py:163 msgid "Ticket has processed - {} ({})" msgstr "チケットが処理済み- {} ({})" @@ -7337,11 +7572,11 @@ msgstr "'{}'という名前の権限は既に存在します" msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" -#: tickets/templates/tickets/_msg_ticket.html:20 +#: tickets/templates/tickets/_msg_ticket.html:21 msgid "View details" msgstr "詳細の表示" -#: tickets/templates/tickets/_msg_ticket.html:25 +#: tickets/templates/tickets/_msg_ticket.html:26 msgid "Direct approval" msgstr "直接承認" @@ -7349,12 +7584,12 @@ msgstr "直接承認" msgid "Ticket information" msgstr "作業指示情報" -#: tickets/templates/tickets/approve_check_password.html:29 +#: tickets/templates/tickets/approve_check_password.html:28 #: tickets/views/approve.py:40 tickets/views/approve.py:77 msgid "Ticket approval" msgstr "作業指示の承認" -#: tickets/templates/tickets/approve_check_password.html:44 +#: tickets/templates/tickets/approve_check_password.html:43 msgid "Approval" msgstr "承認" @@ -7381,11 +7616,11 @@ msgstr "無効な承認アクション" msgid "This user is not authorized to approve this ticket" msgstr "このユーザーはこの作業指示を承認する権限がありません" -#: users/api/user.py:141 +#: users/api/user.py:137 msgid "Can not invite self" msgstr "自分自身を招待することはできません" -#: users/api/user.py:194 +#: users/api/user.py:190 msgid "Could not reset self otp, use profile reset instead" msgstr "自己otpをリセットできませんでした、代わりにプロファイルリセットを使用" @@ -7429,11 +7664,11 @@ msgstr "マルチスクリーンディスプレイ" msgid "Drives redirect" msgstr "ディスクマウント" -#: users/const.py:57 +#: users/const.py:64 msgid "Replace" msgstr "置換" -#: users/const.py:58 +#: users/const.py:65 msgid "Suffix" msgstr "接尾辞を付ける" @@ -7520,8 +7755,8 @@ msgstr "公開鍵は古いものと同じであってはなりません。" msgid "Not a valid ssh public key" msgstr "有効なssh公開鍵ではありません" -#: users/forms/profile.py:173 users/models/user.py:826 -#: xpack/plugins/cloud/serializers/account_attrs.py:203 +#: users/forms/profile.py:173 users/models/user.py:833 +#: xpack/plugins/cloud/serializers/account_attrs.py:210 msgid "Public key" msgstr "公開キー" @@ -7529,72 +7764,74 @@ msgstr "公開キー" msgid "Preference" msgstr "ユーザー設定" -#: users/models/user.py:643 users/serializers/profile.py:94 +#: users/models/user.py:646 users/serializers/profile.py:94 msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:805 users/serializers/user.py:169 +#: users/models/user.py:812 users/serializers/user.py:169 msgid "Is service account" msgstr "サービスアカウントです" -#: users/models/user.py:807 +#: users/models/user.py:814 msgid "Avatar" msgstr "アバター" -#: users/models/user.py:810 +#: users/models/user.py:817 msgid "Wechat" msgstr "微信" -#: users/models/user.py:813 users/serializers/user.py:106 +#: users/models/user.py:820 users/serializers/user.py:106 msgid "Phone" msgstr "電話" -#: users/models/user.py:819 +#: users/models/user.py:826 msgid "OTP secret key" msgstr "OTP 秘密" -#: users/models/user.py:831 users/serializers/profile.py:128 +# msgid "Private key" +# msgstr "ssh秘密鍵" +#: users/models/user.py:838 users/serializers/profile.py:128 #: users/serializers/user.py:166 msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:841 +#: users/models/user.py:848 msgid "Date password last updated" msgstr "最終更新日パスワード" -#: users/models/user.py:844 +#: users/models/user.py:851 msgid "Need update password" msgstr "更新パスワードが必要" -#: users/models/user.py:846 +#: users/models/user.py:853 msgid "Date api key used" msgstr "Api key 最後に使用した日付" -#: users/models/user.py:969 +#: users/models/user.py:984 msgid "Can not delete admin user" msgstr "管理者ユーザーを削除できませんでした" -#: users/models/user.py:995 +#: users/models/user.py:1011 msgid "Can invite user" msgstr "ユーザーを招待できます" -#: users/models/user.py:996 +#: users/models/user.py:1012 msgid "Can remove user" msgstr "ユーザーを削除できます" -#: users/models/user.py:997 +#: users/models/user.py:1013 msgid "Can match user" msgstr "ユーザーに一致できます" -#: users/models/user.py:1006 +#: users/models/user.py:1022 msgid "Administrator" msgstr "管理者" -#: users/models/user.py:1009 +#: users/models/user.py:1025 msgid "Administrator is the super user of system" msgstr "管理者はシステムのスーパーユーザーです" -#: users/models/user.py:1034 +#: users/models/user.py:1050 msgid "User password history" msgstr "ユーザーパスワード履歴" @@ -7666,37 +7903,49 @@ msgid "RDP client option" msgstr "RDPクライアントオプション" #: users/serializers/preference/luna.py:45 +msgid "RDP color quality" +msgstr "" + +#: users/serializers/preference/luna.py:49 msgid "Rdp smart size" msgstr "RDPインテリジェントサイズ" -#: users/serializers/preference/luna.py:46 +#: users/serializers/preference/luna.py:50 msgid "" "Determines whether the client computer should scale the content on the " "remote computer to fit the window size of the client computer when the " "window is resized." -msgstr "ウィンドウサイズを変更するときにクライアントコンピュータがクライアントコンピュータのウィンドウサイズに合わせるためにリモートコンピュータ上のコンテンツをスケーリングすべきかどうかを判断する" +msgstr "" -#: users/serializers/preference/luna.py:51 +# msgid "" +# "Determines whether the client computer should scale the content on the " +# "remote computer to fit the window size of the client computer when the " +# "window is resized." +# msgstr "" +# "ウィンドウサイズを変更するときにクライアントコンピュータがクライアントコン" +# "ピュータのウィンドウサイズに合わせるためにリモートコンピュータ上のコンテンツ" +# "をスケーリングすべきかどうかを判断する" +#: users/serializers/preference/luna.py:55 msgid "Remote application connection method" msgstr "リモートアプリケーション接続方式" -#: users/serializers/preference/luna.py:58 +#: users/serializers/preference/luna.py:62 msgid "Character terminal font size" msgstr "文字終端フォントサイズ" -#: users/serializers/preference/luna.py:61 +#: users/serializers/preference/luna.py:65 msgid "Backspace as Ctrl+H" msgstr "文字終端Backspace As Ctrl+H" -#: users/serializers/preference/luna.py:64 +#: users/serializers/preference/luna.py:68 msgid "Right click quickly paste" msgstr "右クリックでクイック貼り付け" -#: users/serializers/preference/luna.py:70 +#: users/serializers/preference/luna.py:74 msgid "Graphics" msgstr "図形化" -#: users/serializers/preference/luna.py:71 +#: users/serializers/preference/luna.py:75 msgid "Command line" msgstr "コマンドライン" @@ -7756,15 +8005,15 @@ msgstr "アバターURL" msgid "MFA level" msgstr "MFA レベル" -#: users/serializers/user.py:282 +#: users/serializers/user.py:287 msgid "Select users" msgstr "ユーザーの選択" -#: users/serializers/user.py:283 +#: users/serializers/user.py:288 msgid "For security, only list several users" msgstr "セキュリティのために、複数のユーザーのみをリストします" -#: users/serializers/user.py:316 +#: users/serializers/user.py:321 msgid "name not unique" msgstr "名前が一意ではない" @@ -7795,9 +8044,9 @@ msgstr "ユーザーの有効期限の定期的な検出" #: users/tasks.py:84 msgid "Check unused users" -msgstr "未使用のユーザーを確認する" +msgstr "未使用のユーザーのチェック" -#: users/tasks.py:114 +#: users/tasks.py:123 msgid "The user has not logged in recently and has been disabled." msgstr "ユーザーは最近ログインしておらず、無効になっています。" @@ -8063,6 +8312,11 @@ msgstr "パスワードの成功をリセットし、ログインページに戻 msgid "XPACK" msgstr "XPack" +#: xpack/exceptions.py:7 +msgid "" +"The current task is not synchronized with unmatched policy assets, skipping" +msgstr "" + #: xpack/plugins/cloud/api.py:56 msgid "Test connection successful" msgstr "テスト接続成功" @@ -8143,47 +8397,67 @@ msgstr "スカイウィング私有雲" msgid "OpenStack" msgstr "OpenStack" -#: xpack/plugins/cloud/const.py:28 +#: xpack/plugins/cloud/const.py:28 xpack/plugins/cloud/providers/zstack.py:21 +msgid "ZStack" +msgstr "ZStack" + +#: xpack/plugins/cloud/const.py:29 msgid "Fusion Compute" msgstr "融合計算" -#: xpack/plugins/cloud/const.py:33 +#: xpack/plugins/cloud/const.py:30 +msgid "SCP" +msgstr "SCP" + +#: xpack/plugins/cloud/const.py:31 +msgid "Apsara Stack" +msgstr "Apsara Stack" + +#: xpack/plugins/cloud/const.py:36 msgid "Private IP" msgstr "プライベートIP" -#: xpack/plugins/cloud/const.py:34 +#: xpack/plugins/cloud/const.py:37 msgid "Public IP" msgstr "パブリックIP" -#: xpack/plugins/cloud/const.py:38 xpack/plugins/cloud/models.py:295 +#: xpack/plugins/cloud/const.py:41 xpack/plugins/cloud/models.py:303 msgid "Instance name" msgstr "インスタンス名" -#: xpack/plugins/cloud/const.py:39 +#: xpack/plugins/cloud/const.py:42 msgid "Instance name and Partial IP" msgstr "インスタンス名と部分IP" -#: xpack/plugins/cloud/const.py:44 +#: xpack/plugins/cloud/const.py:47 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:48 +#: xpack/plugins/cloud/const.py:51 msgid "Unsync" msgstr "同期していません" -#: xpack/plugins/cloud/const.py:49 +#: xpack/plugins/cloud/const.py:52 msgid "New Sync" msgstr "新しい同期" -#: xpack/plugins/cloud/const.py:50 +#: xpack/plugins/cloud/const.py:53 msgid "Synced" msgstr "同期済み" -#: xpack/plugins/cloud/const.py:51 +#: xpack/plugins/cloud/const.py:54 msgid "Released" msgstr "リリース済み" -#: xpack/plugins/cloud/manager.py:54 +#: xpack/plugins/cloud/const.py:58 +msgid "And" +msgstr "そして" + +#: xpack/plugins/cloud/const.py:59 +msgid "Or" +msgstr "または" + +#: xpack/plugins/cloud/manager.py:56 msgid "Account unavailable" msgstr "利用できないアカウント" @@ -8207,7 +8481,7 @@ msgstr "クラウドアカウント" msgid "Test cloud account" msgstr "クラウドアカウントのテスト" -#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:151 +#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:159 msgid "Regions" msgstr "リージョン" @@ -8216,122 +8490,134 @@ msgid "Hostname strategy" msgstr "ホスト名戦略" #: xpack/plugins/cloud/models.py:100 -#: xpack/plugins/cloud/serializers/task.py:154 +#: xpack/plugins/cloud/serializers/task.py:162 msgid "IP network segment group" msgstr "IPネットワークセグメントグループ" #: xpack/plugins/cloud/models.py:103 -#: xpack/plugins/cloud/serializers/task.py:159 +#: xpack/plugins/cloud/serializers/task.py:167 msgid "Sync IP type" msgstr "同期IPタイプ" #: xpack/plugins/cloud/models.py:106 -#: xpack/plugins/cloud/serializers/task.py:177 +#: xpack/plugins/cloud/serializers/task.py:185 msgid "Always update" msgstr "常に更新" -#: xpack/plugins/cloud/models.py:112 +#: xpack/plugins/cloud/models.py:108 +msgid "Fully synchronous" +msgstr "完全同期" + +#: xpack/plugins/cloud/models.py:113 msgid "Date last sync" msgstr "最終同期日" -#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/models.py:313 -#: xpack/plugins/cloud/models.py:337 +#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/models.py:321 +#: xpack/plugins/cloud/models.py:345 msgid "Strategy" msgstr "戦略" -#: xpack/plugins/cloud/models.py:120 xpack/plugins/cloud/models.py:197 +#: xpack/plugins/cloud/models.py:121 xpack/plugins/cloud/models.py:200 msgid "Sync instance task" msgstr "インスタンスの同期タスク" -#: xpack/plugins/cloud/models.py:208 xpack/plugins/cloud/models.py:260 +#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:263 msgid "Date sync" msgstr "日付の同期" -#: xpack/plugins/cloud/models.py:212 +#: xpack/plugins/cloud/models.py:215 msgid "Sync instance snapshot" msgstr "インスタンススナップショットの同期" -#: xpack/plugins/cloud/models.py:216 +#: xpack/plugins/cloud/models.py:219 msgid "Sync instance task execution" msgstr "インスタンスタスクの同期実行" -#: xpack/plugins/cloud/models.py:240 +#: xpack/plugins/cloud/models.py:243 msgid "Sync task" msgstr "同期タスク" -#: xpack/plugins/cloud/models.py:244 +#: xpack/plugins/cloud/models.py:247 msgid "Sync instance task history" msgstr "インスタンスタスク履歴の同期" -#: xpack/plugins/cloud/models.py:247 +#: xpack/plugins/cloud/models.py:250 msgid "Instance" msgstr "インスタンス" -#: xpack/plugins/cloud/models.py:264 +#: xpack/plugins/cloud/models.py:267 msgid "Sync instance detail" msgstr "同期インスタンスの詳細" -#: xpack/plugins/cloud/models.py:281 +#: xpack/plugins/cloud/models.py:279 xpack/plugins/cloud/serializers/task.py:72 +msgid "Rule relation" +msgstr "条件関係" + +#: xpack/plugins/cloud/models.py:288 msgid "Task strategy" msgstr "ミッション戦略です" -#: xpack/plugins/cloud/models.py:285 +#: xpack/plugins/cloud/models.py:292 msgid "Equal" msgstr "等しい" -#: xpack/plugins/cloud/models.py:286 +#: xpack/plugins/cloud/models.py:293 msgid "Not Equal" msgstr "不等于" -#: xpack/plugins/cloud/models.py:287 +#: xpack/plugins/cloud/models.py:294 msgid "In" msgstr "で..." -#: xpack/plugins/cloud/models.py:288 +#: xpack/plugins/cloud/models.py:295 msgid "Contains" msgstr "含む" -#: xpack/plugins/cloud/models.py:289 +#: xpack/plugins/cloud/models.py:296 +msgid "Exclude" +msgstr "除外" + +#: xpack/plugins/cloud/models.py:297 msgid "Startswith" msgstr "始まる..." -#: xpack/plugins/cloud/models.py:290 +#: xpack/plugins/cloud/models.py:298 msgid "Endswith" msgstr "終わる..." -#: xpack/plugins/cloud/models.py:296 +#: xpack/plugins/cloud/models.py:304 msgid "Instance platform" msgstr "インスタンス名" -#: xpack/plugins/cloud/models.py:297 +#: xpack/plugins/cloud/models.py:305 msgid "Instance address" msgstr "インスタンスアドレス" -#: xpack/plugins/cloud/models.py:304 +#: xpack/plugins/cloud/models.py:312 msgid "Rule attr" msgstr "ルール属性" -#: xpack/plugins/cloud/models.py:308 +#: xpack/plugins/cloud/models.py:316 msgid "Rule match" msgstr "ルール一致" -#: xpack/plugins/cloud/models.py:310 +#: xpack/plugins/cloud/models.py:318 msgid "Rule value" msgstr "ルール値" -#: xpack/plugins/cloud/models.py:317 xpack/plugins/cloud/serializers/task.py:70 +#: xpack/plugins/cloud/models.py:325 xpack/plugins/cloud/serializers/task.py:75 msgid "Strategy rule" msgstr "戦略ルール" -#: xpack/plugins/cloud/models.py:332 +#: xpack/plugins/cloud/models.py:340 msgid "Action attr" msgstr "アクション属性" -#: xpack/plugins/cloud/models.py:334 +#: xpack/plugins/cloud/models.py:342 msgid "Action value" msgstr "アクション値" -#: xpack/plugins/cloud/models.py:341 xpack/plugins/cloud/serializers/task.py:73 +#: xpack/plugins/cloud/models.py:349 xpack/plugins/cloud/serializers/task.py:78 msgid "Strategy action" msgstr "戦略アクション" @@ -8525,11 +8811,11 @@ msgstr "TR-Istanbul" msgid "CN East-Suqian" msgstr "華東-宿遷" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:68 msgid "Validity display" msgstr "有効表示" -#: xpack/plugins/cloud/serializers/account.py:65 +#: xpack/plugins/cloud/serializers/account.py:69 msgid "Provider display" msgstr "プロバイダ表示" @@ -8546,50 +8832,50 @@ msgid "Subscription ID" msgstr "サブスクリプションID" #: xpack/plugins/cloud/serializers/account_attrs.py:98 -#: xpack/plugins/cloud/serializers/account_attrs.py:103 -#: xpack/plugins/cloud/serializers/account_attrs.py:119 -#: xpack/plugins/cloud/serializers/account_attrs.py:149 -#: xpack/plugins/cloud/serializers/account_attrs.py:199 +#: xpack/plugins/cloud/serializers/account_attrs.py:102 +#: xpack/plugins/cloud/serializers/account_attrs.py:126 +#: xpack/plugins/cloud/serializers/account_attrs.py:156 +#: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "API Endpoint" msgstr "APIエンドポイント" -#: xpack/plugins/cloud/serializers/account_attrs.py:109 +#: xpack/plugins/cloud/serializers/account_attrs.py:108 msgid "Auth url" msgstr "認証アドレス" -#: xpack/plugins/cloud/serializers/account_attrs.py:110 +#: xpack/plugins/cloud/serializers/account_attrs.py:109 msgid "eg: http://openstack.example.com:5000/v3" msgstr "例えば: http://openstack.example.com:5000/v3" -#: xpack/plugins/cloud/serializers/account_attrs.py:113 +#: xpack/plugins/cloud/serializers/account_attrs.py:112 msgid "User domain" msgstr "ユーザードメイン" -#: xpack/plugins/cloud/serializers/account_attrs.py:120 +#: xpack/plugins/cloud/serializers/account_attrs.py:127 msgid "Cert File" msgstr "証明書ファイル" -#: xpack/plugins/cloud/serializers/account_attrs.py:121 +#: xpack/plugins/cloud/serializers/account_attrs.py:128 msgid "Key File" msgstr "キーファイル" -#: xpack/plugins/cloud/serializers/account_attrs.py:137 +#: xpack/plugins/cloud/serializers/account_attrs.py:144 msgid "Service account key" msgstr "サービスアカウントキー" -#: xpack/plugins/cloud/serializers/account_attrs.py:138 +#: xpack/plugins/cloud/serializers/account_attrs.py:145 msgid "The file is in JSON format" msgstr "ファイルはJSON形式です。" -#: xpack/plugins/cloud/serializers/account_attrs.py:156 +#: xpack/plugins/cloud/serializers/account_attrs.py:163 msgid "IP address invalid `{}`, {}" msgstr "IPアドレスが無効: '{}', {}" -#: xpack/plugins/cloud/serializers/account_attrs.py:172 +#: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Such as: 192.168.1.0/24, 10.0.0.0-10.0.0.255" msgstr "例:192.168.1.0/24、10.0.0.0.0-10.0.0.255" -#: xpack/plugins/cloud/serializers/account_attrs.py:175 +#: xpack/plugins/cloud/serializers/account_attrs.py:182 msgid "" "The port is used to detect the validity of the IP address. When the " "synchronization task is executed, only the valid IP address will be " @@ -8599,27 +8885,27 @@ msgstr "" "実行されると、有効な IP アドレスのみが同期されます。
ポートが0の場合、す" "べてのIPアドレスが有効です。" -#: xpack/plugins/cloud/serializers/account_attrs.py:183 +#: xpack/plugins/cloud/serializers/account_attrs.py:190 msgid "Hostname prefix" msgstr "ホスト名プレフィックス" -#: xpack/plugins/cloud/serializers/account_attrs.py:186 +#: xpack/plugins/cloud/serializers/account_attrs.py:193 msgid "IP segment" msgstr "IP セグメント" -#: xpack/plugins/cloud/serializers/account_attrs.py:190 +#: xpack/plugins/cloud/serializers/account_attrs.py:197 msgid "Test port" msgstr "テストポート" -#: xpack/plugins/cloud/serializers/account_attrs.py:193 +#: xpack/plugins/cloud/serializers/account_attrs.py:200 msgid "Test timeout" msgstr "テストタイムアウト" -#: xpack/plugins/cloud/serializers/account_attrs.py:209 +#: xpack/plugins/cloud/serializers/account_attrs.py:216 msgid "Project" msgstr "project" -#: xpack/plugins/cloud/serializers/task.py:143 +#: xpack/plugins/cloud/serializers/task.py:151 msgid "" "Only instances matching the IP range will be synced.
If the instance " "contains multiple IP addresses, the first IP address that matches will be " @@ -8633,11 +8919,11 @@ msgstr "" "ドレスをランダムに一致させることを意味します。
例: " "192.168.1.0/24,10.1.1.1-10.1.1.20。" -#: xpack/plugins/cloud/serializers/task.py:149 +#: xpack/plugins/cloud/serializers/task.py:157 msgid "History count" msgstr "実行回数" -#: xpack/plugins/cloud/serializers/task.py:150 +#: xpack/plugins/cloud/serializers/task.py:158 msgid "Instance count" msgstr "インスタンス数" @@ -8681,7 +8967,15 @@ msgstr "ログアウトページのロゴ" msgid "Theme" msgstr "テーマ" -#: xpack/plugins/interface/models.py:44 xpack/plugins/interface/models.py:85 +#: xpack/plugins/interface/models.py:42 +msgid "Beian link" +msgstr "公安オンライン申告ジャンプリンク" + +#: xpack/plugins/interface/models.py:43 +msgid "Beian text" +msgstr "公安網登録番号" + +#: xpack/plugins/interface/models.py:46 xpack/plugins/interface/models.py:87 msgid "Interface setting" msgstr "インターフェイスの設定" @@ -8713,17 +9007,17 @@ msgstr "エンタープライズプロフェッショナル版" msgid "Ultimate edition" msgstr "エンタープライズ・フラッグシップ・エディション" -#~ msgid "Copy" -#~ msgstr "コピー" +#~ msgid "Password can not contains `{{` or `}}`" +#~ msgstr "パスワードには `{` または `}` 文字を含めることはできません" -#~ msgid "Paste" -#~ msgstr "貼り付け" +#~ msgid "Password can not contains `{%` or `%}`" +#~ msgstr "パスワードには `{%` または `%}` 文字を含めることはできません" -#~ msgid "Password can not contains `'` " -#~ msgstr "パスワードには `'` を含まない" +#~ msgid "FeiShu query user failed" +#~ msgstr "FeiShuクエリユーザーが失敗しました" -#~ msgid "Password can not contains `\"` " -#~ msgstr "パスワードには `\"` を含まない" +#~ msgid "The FeiShu is already bound to another user" +#~ msgstr "FeiShuはすでに別のユーザーにバインドされています" -#~ msgid "Object Storage" -#~ msgstr "オブジェクトストレージ" +#~ msgid "Binding FeiShu successfully" +#~ msgstr "本を飛ばすのバインドに成功" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index ef1eef0a1..abcc5b418 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8324031d40c7cbcd9e332221ee8f607396836b87907847b7f94e879269cc97c8 -size 135739 +oid sha256:853320b42ac7795fa983c4216f89b3a776a889453d90e11dc425d1388ff9b803 +size 139012 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index dff3dd204..bd5989a79 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-11-16 15:05+0800\n" +"POT-Creation-Date: 2023-12-21 16:12+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -48,7 +48,7 @@ msgstr "Access key" #: accounts/const/account.py:9 assets/models/_user.py:48 #: authentication/backends/passkey/models.py:16 -#: authentication/models/sso_token.py:14 settings/serializers/feature.py:50 +#: authentication/models/sso_token.py:14 settings/serializers/feature.py:52 msgid "Token" msgstr "Token" @@ -73,7 +73,7 @@ msgstr "同名账号" msgid "Anonymous account" msgstr "匿名账号" -#: accounts/const/account.py:25 users/models/user.py:739 +#: accounts/const/account.py:25 users/models/user.py:742 msgid "Local" msgstr "数据库" @@ -86,11 +86,11 @@ msgstr "收集" msgid "Template" msgstr "模板" -#: accounts/const/account.py:31 ops/const.py:45 +#: accounts/const/account.py:31 ops/const.py:46 msgid "Skip" msgstr "跳过" -#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:237 +#: accounts/const/account.py:32 audits/const.py:24 rbac/tree.py:239 #: templates/_csv_import_export.html:18 templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" @@ -98,11 +98,11 @@ msgstr "更新" #: accounts/const/account.py:33 #: accounts/serializers/automations/change_secret.py:150 audits/const.py:62 #: audits/signal_handlers/activity_log.py:33 common/const/choices.py:19 -#: ops/const.py:74 terminal/const.py:78 xpack/plugins/cloud/const.py:43 +#: ops/const.py:75 terminal/const.py:79 xpack/plugins/cloud/const.py:46 msgid "Failed" msgstr "失败" -#: accounts/const/automation.py:24 rbac/tree.py:50 +#: accounts/const/automation.py:24 rbac/tree.py:52 msgid "Push account" msgstr "账号推送" @@ -114,94 +114,99 @@ msgstr "更改密码" msgid "Verify account" msgstr "验证账号" -#: accounts/const/automation.py:27 +#: accounts/const/automation.py:27 accounts/tasks/remove_account.py:14 +#: accounts/tasks/remove_account.py:23 +msgid "Remove account" +msgstr "移除账号" + +#: accounts/const/automation.py:28 msgid "Gather accounts" msgstr "收集账号" -#: accounts/const/automation.py:28 +#: accounts/const/automation.py:29 msgid "Verify gateway account" msgstr "验证网关账号" -#: accounts/const/automation.py:46 +#: accounts/const/automation.py:47 msgid "Specific secret" msgstr "指定" -#: accounts/const/automation.py:47 +#: accounts/const/automation.py:48 msgid "Random generate" msgstr "随机生成" -#: accounts/const/automation.py:51 ops/const.py:13 +#: accounts/const/automation.py:52 ops/const.py:13 msgid "Append SSH KEY" msgstr "追加" -#: accounts/const/automation.py:52 ops/const.py:14 +#: accounts/const/automation.py:53 ops/const.py:14 msgid "Empty and append SSH KEY" msgstr "清空所有并添加" -#: accounts/const/automation.py:53 ops/const.py:15 +#: accounts/const/automation.py:54 ops/const.py:15 msgid "Replace (Replace only keys pushed by JumpServer) " msgstr "替换 (只替换由 JumpServer 推送的密钥)" -#: accounts/const/automation.py:58 +#: accounts/const/automation.py:59 msgid "On asset create" msgstr "资产创建时" -#: accounts/const/automation.py:61 +#: accounts/const/automation.py:62 msgid "On perm add user" msgstr "授权变更时添加用户" -#: accounts/const/automation.py:63 +#: accounts/const/automation.py:64 msgid "On perm add user group" msgstr "授权变更时添加用户组" -#: accounts/const/automation.py:65 +#: accounts/const/automation.py:66 msgid "On perm add asset" msgstr "授权变更时添加资产" -#: accounts/const/automation.py:67 +#: accounts/const/automation.py:68 msgid "On perm add node" msgstr "授权变更时添加节点" -#: accounts/const/automation.py:69 +#: accounts/const/automation.py:70 msgid "On perm add account" msgstr "授权变更时添加账号" -#: accounts/const/automation.py:71 +#: accounts/const/automation.py:72 msgid "On asset join node" msgstr "资产变更时添加到节点" -#: accounts/const/automation.py:73 +#: accounts/const/automation.py:74 msgid "On user join group" msgstr "用户变更时添加到用户组" -#: accounts/const/automation.py:81 +#: accounts/const/automation.py:82 msgid "On perm change" msgstr "授权变更时" -#: accounts/const/automation.py:88 +#: accounts/const/automation.py:89 msgid "Inherit from group or node" msgstr "继承自用户组或资产节点" -#: accounts/const/automation.py:96 +#: accounts/const/automation.py:97 msgid "Create and push" msgstr "创建并推送" -#: accounts/const/automation.py:97 +#: accounts/const/automation.py:98 msgid "Only create" msgstr "仅创建" -#: accounts/const/automation.py:102 +#: accounts/const/automation.py:103 #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/msg.py:22 #: settings/serializers/msg.py:57 users/forms/profile.py:102 -#: users/forms/profile.py:109 users/models/user.py:795 +#: users/forms/profile.py:109 users/models/user.py:802 #: users/templates/users/forgot_password.html:117 #: users/views/profile/reset.py:92 msgid "Email" msgstr "邮箱" -#: accounts/const/automation.py:104 terminal/const.py:86 +#: accounts/const/automation.py:105 terminal/const.py:87 msgid "SFTP" msgstr "SFTP" @@ -210,7 +215,7 @@ msgstr "SFTP" msgid "Database" msgstr "数据库" -#: accounts/const/vault.py:9 settings/serializers/feature.py:41 +#: accounts/const/vault.py:9 settings/serializers/feature.py:43 msgid "HCP Vault" msgstr "HashiCorp Vault" @@ -232,59 +237,60 @@ msgstr "导出搜素: %s" msgid "User %s view/export secret" msgstr "用户 %s 查看/导出 了密码" -#: accounts/models/account.py:48 +#: accounts/models/account.py:49 #: accounts/models/automations/gather_account.py:16 -#: accounts/serializers/account/account.py:210 -#: accounts/serializers/account/account.py:255 +#: accounts/serializers/account/account.py:213 +#: accounts/serializers/account/account.py:258 #: accounts/serializers/account/gathered_account.py:10 #: accounts/serializers/automations/change_secret.py:106 #: accounts/serializers/automations/change_secret.py:126 #: accounts/templates/accounts/asset_account_change_info.html:7 -#: acls/serializers/base.py:123 assets/models/asset/common.py:93 -#: assets/models/asset/common.py:342 assets/models/cmd_filter.py:36 -#: assets/serializers/domain.py:19 assets/serializers/label.py:27 -#: audits/models.py:58 authentication/models/connection_token.py:36 -#: perms/models/asset_permission.py:68 perms/serializers/permission.py:34 +#: acls/serializers/base.py:123 assets/models/asset/common.py:95 +#: assets/models/asset/common.py:350 assets/models/cmd_filter.py:36 +#: assets/serializers/domain.py:20 audits/models.py:58 +#: authentication/models/connection_token.py:36 +#: perms/models/asset_permission.py:69 perms/serializers/permission.py:36 #: terminal/backends/command/models.py:17 terminal/models/session/session.py:31 #: terminal/notifications.py:155 terminal/serializers/command.py:17 #: terminal/serializers/session.py:26 #: terminal/templates/terminal/_msg_command_warning.html:4 #: terminal/templates/terminal/_msg_session_sharing.html:4 -#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:253 +#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:256 msgid "Asset" msgstr "资产" -#: accounts/models/account.py:52 accounts/models/template.py:15 -#: accounts/serializers/account/account.py:217 -#: accounts/serializers/account/account.py:265 -#: accounts/serializers/account/template.py:24 -#: authentication/serializers/connect_token_secret.py:49 +#: accounts/models/account.py:53 accounts/models/template.py:16 +#: accounts/serializers/account/account.py:220 +#: accounts/serializers/account/account.py:268 +#: accounts/serializers/account/template.py:27 +#: authentication/serializers/connect_token_secret.py:50 msgid "Su from" msgstr "切换自" -#: accounts/models/account.py:54 assets/const/protocol.py:169 +#: accounts/models/account.py:55 assets/const/protocol.py:169 #: settings/serializers/auth/cas.py:20 settings/serializers/auth/feishu.py:20 #: terminal/models/applet/applet.py:35 +#: terminal/models/virtualapp/virtualapp.py:21 msgid "Version" msgstr "版本" -#: accounts/models/account.py:56 accounts/serializers/account/account.py:212 -#: users/models/user.py:838 +#: accounts/models/account.py:57 accounts/serializers/account/account.py:215 +#: users/models/user.py:845 msgid "Source" msgstr "来源" -#: accounts/models/account.py:57 +#: accounts/models/account.py:58 msgid "Source ID" msgstr "来源 ID" -#: accounts/models/account.py:60 +#: accounts/models/account.py:61 #: accounts/serializers/automations/change_secret.py:107 #: accounts/serializers/automations/change_secret.py:127 #: acls/serializers/base.py:124 acls/templates/acls/asset_login_reminder.html:7 #: assets/serializers/asset/common.py:125 assets/serializers/gateway.py:28 -#: audits/models.py:59 authentication/api/connection_token.py:403 -#: ops/models/base.py:18 perms/models/asset_permission.py:74 -#: perms/serializers/permission.py:39 terminal/backends/command/models.py:18 +#: audits/models.py:59 authentication/api/connection_token.py:405 +#: ops/models/base.py:18 perms/models/asset_permission.py:75 +#: perms/serializers/permission.py:41 terminal/backends/command/models.py:18 #: terminal/models/session/session.py:33 #: terminal/templates/terminal/_msg_command_warning.html:8 #: terminal/templates/terminal/_msg_session_sharing.html:8 @@ -292,26 +298,30 @@ msgstr "来源 ID" msgid "Account" msgstr "账号" -#: accounts/models/account.py:66 +#: accounts/models/account.py:67 msgid "Can view asset account secret" msgstr "可以查看资产账号密码" -#: accounts/models/account.py:67 +#: accounts/models/account.py:68 msgid "Can view asset history account" msgstr "可以查看资产历史账号" -#: accounts/models/account.py:68 +#: accounts/models/account.py:69 msgid "Can view asset history account secret" msgstr "可以查看资产历史账号密码" -#: accounts/models/account.py:69 +#: accounts/models/account.py:70 msgid "Can verify account" msgstr "可以验证账号" -#: accounts/models/account.py:70 +#: accounts/models/account.py:71 msgid "Can push account" msgstr "可以推送账号" +#: accounts/models/account.py:72 +msgid "Can remove account" +msgstr "可以移除账号" + #: accounts/models/automations/backup_account.py:27 msgid "Backup Type" msgstr "备份类型" @@ -349,9 +359,9 @@ msgstr "账号备份计划" #: accounts/models/automations/backup_account.py:119 #: assets/models/automations/base.py:115 audits/models.py:65 -#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:231 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:235 #: ops/templates/ops/celery_task_log.html:75 -#: perms/models/asset_permission.py:77 terminal/models/applet/host.py:141 +#: perms/models/asset_permission.py:78 terminal/models/applet/host.py:141 #: terminal/models/session/session.py:44 #: tickets/models/ticket/apply_application.py:30 #: tickets/models/ticket/apply_asset.py:19 @@ -377,14 +387,14 @@ msgid "Trigger mode" msgstr "触发模式" #: accounts/models/automations/backup_account.py:133 audits/models.py:203 -#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:205 +#: terminal/models/session/sharing.py:125 xpack/plugins/cloud/models.py:208 msgid "Reason" msgstr "原因" #: accounts/models/automations/backup_account.py:135 #: accounts/serializers/automations/change_secret.py:105 #: accounts/serializers/automations/change_secret.py:128 -#: ops/serializers/job.py:56 terminal/serializers/session.py:49 +#: ops/serializers/job.py:64 terminal/serializers/session.py:49 msgid "Is success" msgstr "是否成功" @@ -457,28 +467,30 @@ msgstr "开始日期" #: accounts/models/automations/change_secret.py:42 #: assets/models/automations/base.py:116 ops/models/base.py:56 -#: ops/models/celery.py:64 ops/models/job.py:232 +#: ops/models/celery.py:64 ops/models/job.py:236 #: terminal/models/applet/host.py:142 msgid "Date finished" msgstr "结束日期" #: accounts/models/automations/change_secret.py:43 #: assets/models/automations/base.py:113 audits/models.py:208 -#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:223 -#: terminal/models/applet/applet.py:318 terminal/models/applet/host.py:140 -#: terminal/models/component/status.py:30 terminal/serializers/applet.py:18 -#: terminal/serializers/applet_host.py:124 tickets/models/ticket/general.py:283 +#: audits/serializers.py:54 ops/models/base.py:49 ops/models/job.py:227 +#: terminal/models/applet/applet.py:319 terminal/models/applet/host.py:140 +#: terminal/models/component/status.py:30 +#: terminal/models/virtualapp/virtualapp.py:99 +#: terminal/serializers/applet.py:18 terminal/serializers/applet_host.py:136 +#: terminal/serializers/virtualapp.py:35 tickets/models/ticket/general.py:283 #: tickets/serializers/super_ticket.py:13 -#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:201 -#: xpack/plugins/cloud/models.py:257 +#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:204 +#: xpack/plugins/cloud/models.py:260 msgid "Status" msgstr "状态" #: accounts/models/automations/change_secret.py:44 -#: accounts/serializers/account/account.py:257 assets/const/automation.py:8 +#: accounts/serializers/account/account.py:260 assets/const/automation.py:8 #: authentication/templates/authentication/passkey.html:173 -#: authentication/views/base.py:27 authentication/views/base.py:28 -#: authentication/views/base.py:29 common/const/choices.py:20 +#: authentication/views/base.py:42 authentication/views/base.py:43 +#: authentication/views/base.py:44 common/const/choices.py:20 msgid "Error" msgstr "错误" @@ -504,7 +516,7 @@ msgstr "最后登录日期" #: authentication/templates/authentication/_msg_different_city.html:9 #: authentication/templates/authentication/_msg_oauth_bind.html:9 #: terminal/serializers/storage.py:136 users/forms/profile.py:32 -#: users/forms/profile.py:115 users/models/user.py:791 +#: users/forms/profile.py:115 users/models/user.py:798 #: users/templates/users/_msg_user_created.html:12 #: xpack/plugins/cloud/serializers/account_attrs.py:26 msgid "Username" @@ -533,8 +545,8 @@ msgstr "触发方式" #: accounts/models/automations/push_account.py:16 acls/models/base.py:41 #: acls/serializers/base.py:57 assets/models/cmd_filter.py:81 -#: audits/models.py:92 audits/serializers.py:87 -#: authentication/serializers/connect_token_secret.py:118 +#: audits/models.py:92 audits/serializers.py:84 +#: authentication/serializers/connect_token_secret.py:119 #: authentication/templates/authentication/_access_key_modal.html:34 msgid "Action" msgstr "动作" @@ -548,17 +560,17 @@ msgid "Verify asset account" msgstr "账号验证" #: accounts/models/base.py:37 accounts/models/base.py:67 -#: accounts/serializers/account/account.py:437 -#: accounts/serializers/account/base.py:16 +#: accounts/serializers/account/account.py:440 +#: accounts/serializers/account/base.py:17 #: accounts/serializers/automations/change_secret.py:45 -#: authentication/serializers/connect_token_secret.py:41 -#: authentication/serializers/connect_token_secret.py:50 +#: authentication/serializers/connect_token_secret.py:42 +#: authentication/serializers/connect_token_secret.py:51 #: terminal/serializers/storage.py:140 msgid "Secret type" msgstr "密文类型" #: accounts/models/base.py:39 accounts/models/mixins/vault.py:49 -#: accounts/serializers/account/base.py:19 +#: accounts/serializers/account/base.py:20 #: authentication/models/temp_token.py:10 #: authentication/templates/authentication/_access_key_modal.html:31 #: settings/serializers/auth/radius.py:19 @@ -570,7 +582,7 @@ msgstr "密钥" msgid "Secret strategy" msgstr "密文策略" -#: accounts/models/base.py:44 accounts/serializers/account/template.py:21 +#: accounts/models/base.py:44 accounts/serializers/account/template.py:24 #: accounts/serializers/automations/change_secret.py:44 msgid "Password rules" msgstr "密码规则" @@ -578,26 +590,30 @@ msgstr "密码规则" #: accounts/models/base.py:64 accounts/serializers/account/virtual.py:20 #: acls/models/base.py:35 acls/models/base.py:96 acls/models/command_acl.py:21 #: acls/serializers/base.py:35 applications/models.py:9 -#: assets/models/_user.py:22 assets/models/asset/common.py:91 -#: assets/models/asset/common.py:157 assets/models/cmd_filter.py:21 -#: assets/models/domain.py:18 assets/models/group.py:17 -#: assets/models/label.py:18 assets/models/platform.py:15 -#: assets/models/platform.py:88 assets/serializers/asset/common.py:146 +#: assets/models/_user.py:22 assets/models/asset/common.py:93 +#: assets/models/asset/common.py:159 assets/models/cmd_filter.py:21 +#: assets/models/domain.py:19 assets/models/group.py:17 +#: assets/models/label.py:18 assets/models/platform.py:16 +#: assets/models/platform.py:95 assets/serializers/asset/common.py:146 #: assets/serializers/platform.py:118 assets/serializers/platform.py:235 #: authentication/backends/passkey/models.py:10 -#: authentication/serializers/connect_token_secret.py:112 ops/mixin.py:21 -#: ops/models/adhoc.py:20 ops/models/celery.py:15 ops/models/celery.py:57 -#: ops/models/job.py:128 ops/models/playbook.py:28 ops/serializers/job.py:20 -#: orgs/models.py:82 perms/models/asset_permission.py:60 rbac/models/role.py:29 -#: settings/models.py:32 settings/serializers/msg.py:82 +#: authentication/serializers/connect_token_secret.py:113 +#: authentication/serializers/connect_token_secret.py:168 labels/models.py:11 +#: ops/mixin.py:21 ops/models/adhoc.py:20 ops/models/celery.py:15 +#: ops/models/celery.py:57 ops/models/job.py:136 ops/models/playbook.py:28 +#: ops/serializers/job.py:18 orgs/models.py:82 +#: perms/models/asset_permission.py:61 rbac/models/role.py:29 +#: settings/models.py:33 settings/models.py:181 settings/serializers/msg.py:82 #: terminal/models/applet/applet.py:33 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:94 +#: terminal/models/component/endpoint.py:95 #: terminal/models/component/storage.py:26 terminal/models/component/task.py:13 -#: terminal/models/component/terminal.py:84 tickets/api/ticket.py:87 +#: terminal/models/component/terminal.py:84 +#: terminal/models/virtualapp/provider.py:10 +#: terminal/models/virtualapp/virtualapp.py:19 tickets/api/ticket.py:87 #: users/forms/profile.py:33 users/models/group.py:13 -#: users/models/preference.py:11 users/models/user.py:793 -#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:273 -#: xpack/plugins/cloud/serializers/task.py:68 +#: users/models/preference.py:11 users/models/user.py:800 +#: xpack/plugins/cloud/models.py:32 xpack/plugins/cloud/models.py:276 +#: xpack/plugins/cloud/serializers/task.py:70 msgid "Name" msgstr "名称" @@ -605,36 +621,37 @@ msgstr "名称" msgid "Privileged" msgstr "特权账号" -#: accounts/models/base.py:70 assets/models/asset/common.py:164 +#: accounts/models/base.py:70 assets/models/asset/common.py:166 #: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 #: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:116 +#: authentication/serializers/connect_token_secret.py:117 #: terminal/models/applet/applet.py:40 -#: terminal/models/component/endpoint.py:105 users/serializers/user.py:167 +#: terminal/models/component/endpoint.py:106 +#: terminal/models/virtualapp/virtualapp.py:23 users/serializers/user.py:167 msgid "Is active" msgstr "激活" -#: accounts/models/template.py:17 assets/models/_user.py:53 +#: accounts/models/template.py:18 assets/models/_user.py:53 msgid "Auto push" msgstr "自动推送" -#: accounts/models/template.py:20 +#: accounts/models/template.py:21 msgid "Platforms" msgstr "系统平台" -#: accounts/models/template.py:22 +#: accounts/models/template.py:23 msgid "Push params" msgstr "账号推送参数" -#: accounts/models/template.py:25 xpack/plugins/cloud/models.py:325 +#: accounts/models/template.py:26 xpack/plugins/cloud/models.py:333 msgid "Account template" msgstr "账号模版" -#: accounts/models/template.py:30 +#: accounts/models/template.py:31 msgid "Can view asset account template secret" msgstr "可以查看资产账号模版密码" -#: accounts/models/template.py:31 +#: accounts/models/template.py:32 msgid "Can change asset account template secret" msgstr "可以更改资产账号模版密码" @@ -682,11 +699,11 @@ msgstr "{} - 账号备份任务已完成, 详情见附件" #: accounts/notifications.py:24 msgid "" "{} - The account backup passage task has been completed: the encryption " -"password has not been set - please go to personal information -> file " -"encryption password to set the encryption password" +"password has not been set - please go to personal information -> Basic file " +"encryption password for preference settings" msgstr "" -"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" -"置加密密码" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 偏好设置的基本中" +"设置文件加密密码" #: accounts/notifications.py:55 msgid "Notification of implementation result of encryption change plan" @@ -719,24 +736,24 @@ msgstr "立即推送" msgid "Exist policy" msgstr "账号存在策略" -#: accounts/serializers/account/account.py:190 applications/models.py:11 -#: assets/models/label.py:21 assets/models/platform.py:89 -#: assets/serializers/asset/common.py:121 assets/serializers/cagegory.py:12 +#: accounts/serializers/account/account.py:193 applications/models.py:11 +#: assets/models/label.py:21 assets/models/platform.py:96 +#: assets/serializers/asset/common.py:122 assets/serializers/cagegory.py:12 #: assets/serializers/platform.py:140 assets/serializers/platform.py:236 -#: perms/serializers/user_permission.py:25 settings/models.py:34 +#: perms/serializers/user_permission.py:25 settings/models.py:35 #: tickets/models/ticket/apply_application.py:13 users/models/preference.py:12 msgid "Category" msgstr "类别" -#: accounts/serializers/account/account.py:191 +#: accounts/serializers/account/account.py:194 #: accounts/serializers/automations/base.py:54 acls/models/command_acl.py:24 #: acls/serializers/command_acl.py:19 applications/models.py:14 #: assets/models/_user.py:50 assets/models/automations/base.py:20 -#: assets/models/cmd_filter.py:74 assets/models/platform.py:90 -#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:120 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:97 +#: assets/serializers/asset/common.py:123 assets/serializers/platform.py:120 #: assets/serializers/platform.py:139 audits/serializers.py:53 -#: audits/serializers.py:173 -#: authentication/serializers/connect_token_secret.py:125 ops/models/job.py:140 +#: audits/serializers.py:170 +#: authentication/serializers/connect_token_secret.py:126 ops/models/job.py:144 #: perms/serializers/user_permission.py:26 terminal/models/applet/applet.py:39 #: terminal/models/component/storage.py:57 #: terminal/models/component/storage.py:146 terminal/serializers/applet.py:29 @@ -748,76 +765,76 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: accounts/serializers/account/account.py:206 +#: accounts/serializers/account/account.py:209 msgid "Asset not found" msgstr "资产不存在" -#: accounts/serializers/account/account.py:246 +#: accounts/serializers/account/account.py:249 msgid "Has secret" msgstr "已托管密码" -#: accounts/serializers/account/account.py:256 ops/models/celery.py:60 +#: accounts/serializers/account/account.py:259 ops/models/celery.py:60 #: tickets/models/comment.py:13 tickets/models/ticket/general.py:45 #: tickets/models/ticket/general.py:279 tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状态" -#: accounts/serializers/account/account.py:258 +#: accounts/serializers/account/account.py:261 msgid "Changed" msgstr "已修改" -#: accounts/serializers/account/account.py:268 +#: accounts/serializers/account/account.py:271 #: accounts/serializers/automations/base.py:22 acls/models/base.py:97 #: acls/templates/acls/asset_login_reminder.html:6 #: assets/models/automations/base.py:19 #: assets/serializers/automations/base.py:20 -#: authentication/api/connection_token.py:402 ops/models/base.py:17 -#: ops/models/job.py:142 ops/serializers/job.py:21 +#: authentication/api/connection_token.py:404 ops/models/base.py:17 +#: ops/models/job.py:146 ops/serializers/job.py:19 #: terminal/templates/terminal/_msg_command_execute_alert.html:16 msgid "Assets" msgstr "资产" -#: accounts/serializers/account/account.py:323 +#: accounts/serializers/account/account.py:326 msgid "Account already exists" msgstr "账号已存在" -#: accounts/serializers/account/account.py:373 +#: accounts/serializers/account/account.py:376 #, python-format msgid "Asset does not support this secret type: %s" msgstr "资产不支持账号类型: %s" -#: accounts/serializers/account/account.py:405 +#: accounts/serializers/account/account.py:408 msgid "Account has exist" msgstr "账号已存在" -#: accounts/serializers/account/account.py:438 -#: authentication/serializers/connect_token_secret.py:158 +#: accounts/serializers/account/account.py:441 +#: authentication/serializers/connect_token_secret.py:159 #: authentication/templates/authentication/_access_key_modal.html:30 -#: perms/models/perm_node.py:21 users/serializers/group.py:31 +#: perms/models/perm_node.py:21 users/serializers/group.py:32 msgid "ID" msgstr "ID" -#: accounts/serializers/account/account.py:448 acls/serializers/base.py:116 +#: accounts/serializers/account/account.py:451 acls/serializers/base.py:116 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:54 #: audits/models.py:90 audits/models.py:172 audits/models.py:269 -#: audits/serializers.py:174 authentication/models/connection_token.py:32 +#: audits/serializers.py:171 authentication/models/connection_token.py:32 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 -#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:62 -#: perms/serializers/permission.py:30 rbac/builtin.py:124 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:63 +#: perms/serializers/permission.py:32 rbac/builtin.py:124 #: rbac/models/rolebinding.py:49 rbac/serializers/rolebinding.py:17 #: terminal/backends/command/models.py:16 terminal/models/session/session.py:29 #: terminal/models/session/sharing.py:34 terminal/notifications.py:156 #: terminal/notifications.py:205 terminal/serializers/command.py:16 #: terminal/templates/terminal/_msg_command_warning.html:6 #: terminal/templates/terminal/_msg_session_sharing.html:6 -#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:988 -#: users/models/user.py:1024 users/serializers/group.py:18 +#: tickets/models/comment.py:21 users/const.py:14 users/models/user.py:1003 +#: users/models/user.py:1040 users/serializers/group.py:19 msgid "User" msgstr "用户" -#: accounts/serializers/account/account.py:449 +#: accounts/serializers/account/account.py:452 #: authentication/templates/authentication/_access_key_modal.html:33 #: terminal/notifications.py:158 terminal/notifications.py:207 msgid "Date" @@ -844,12 +861,12 @@ msgstr "当前只支持邮件发送" msgid "Asset type" msgstr "资产类型" -#: accounts/serializers/account/base.py:24 terminal/serializers/storage.py:149 +#: accounts/serializers/account/base.py:25 terminal/serializers/storage.py:149 msgid "Key password" msgstr "密钥密码" #: accounts/serializers/account/base.py:78 -#: assets/serializers/asset/common.py:378 +#: assets/serializers/asset/common.py:379 msgid "Spec info" msgstr "特殊信息" @@ -881,15 +898,19 @@ msgstr "数字" msgid "Special symbol" msgstr "特殊字符" -#: accounts/serializers/account/template.py:35 +#: accounts/serializers/account/template.py:19 +msgid "Exclude symbol" +msgstr "排除字符" + +#: accounts/serializers/account/template.py:38 msgid "Secret generation strategy for account creation" msgstr "密码生成策略,用于账号创建时,设置密码" -#: accounts/serializers/account/template.py:36 +#: accounts/serializers/account/template.py:39 msgid "Whether to automatically push the account to the asset" msgstr "是否自动推送账号到资产" -#: accounts/serializers/account/template.py:39 +#: accounts/serializers/account/template.py:42 msgid "" "Associated platform, you can configure push parameters. If not associated, " "default parameters will be used" @@ -898,14 +919,15 @@ msgstr "关联平台,可配置推送参数,如果不关联,将使用默认 #: accounts/serializers/account/virtual.py:19 assets/models/_user.py:27 #: assets/models/cmd_filter.py:40 assets/models/cmd_filter.py:88 #: assets/models/group.py:20 common/db/models.py:36 ops/models/adhoc.py:26 -#: ops/models/job.py:148 ops/models/playbook.py:31 rbac/models/role.py:37 -#: settings/models.py:37 terminal/models/applet/applet.py:45 -#: terminal/models/applet/applet.py:319 terminal/models/applet/host.py:143 -#: terminal/models/component/endpoint.py:24 -#: terminal/models/component/endpoint.py:104 -#: terminal/models/session/session.py:46 tickets/models/comment.py:32 -#: tickets/models/ticket/general.py:297 users/models/user.py:829 -#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:109 +#: ops/models/job.py:152 ops/models/playbook.py:31 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:45 +#: terminal/models/applet/applet.py:320 terminal/models/applet/host.py:143 +#: terminal/models/component/endpoint.py:25 +#: terminal/models/component/endpoint.py:105 +#: terminal/models/session/session.py:46 +#: terminal/models/virtualapp/virtualapp.py:28 tickets/models/comment.py:32 +#: tickets/models/ticket/general.py:297 users/models/user.py:836 +#: xpack/plugins/cloud/models.py:39 xpack/plugins/cloud/models.py:110 msgid "Comment" msgstr "备注" @@ -920,9 +942,7 @@ msgstr "" "CACHE_LOGIN_PASSWORD_ENABLED=true,重启服务才能开启" #: accounts/serializers/automations/base.py:23 -#: assets/models/asset/common.py:163 assets/models/automations/base.py:18 -#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 -#: perms/models/asset_permission.py:71 +#: assets/serializers/automations/base.py:21 msgid "Nodes" msgstr "节点" @@ -955,8 +975,8 @@ msgstr "自动化任务执行历史" #: accounts/serializers/automations/change_secret.py:149 audits/const.py:61 #: audits/models.py:64 audits/signal_handlers/activity_log.py:33 -#: common/const/choices.py:18 ops/const.py:72 ops/serializers/celery.py:40 -#: terminal/const.py:77 terminal/models/session/sharing.py:121 +#: common/const/choices.py:18 ops/const.py:73 ops/serializers/celery.py:46 +#: terminal/const.py:78 terminal/models/session/sharing.py:121 #: tickets/views/approve.py:117 msgid "Success" msgstr "成功" @@ -1020,15 +1040,13 @@ msgstr "新增账号" msgid "Deleted account" msgstr "删除账号" -#: accounts/utils.py:53 -msgid "Password can not contains `{{` or `}}`" -msgstr "密码不能包含 `{{` 或 `}}` 字符" +#: accounts/utils.py:52 +msgid "" +"If the password starts with {{` and ends with }} `, then the password is not " +"allowed." +msgstr "如果密码以 `{{` 开始,并且以 `}}` 结束,则该密码是不允许的。" -#: accounts/utils.py:55 -msgid "Password can not contains `{%` or `%}`" -msgstr "密码不能包含 `{%` 或 `%}` 字符" - -#: accounts/utils.py:66 +#: accounts/utils.py:59 msgid "private key invalid or passphrase error" msgstr "密钥不合法或密钥密码错误" @@ -1037,7 +1055,7 @@ msgid "Acls" msgstr "访问控制" #: acls/const.py:6 audits/const.py:36 terminal/const.py:11 tickets/const.py:45 -#: tickets/templates/tickets/approve_check_password.html:48 +#: tickets/templates/tickets/approve_check_password.html:47 msgid "Reject" msgstr "拒绝" @@ -1058,26 +1076,26 @@ msgid "Notifications" msgstr "通知" #: acls/models/base.py:37 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:97 -#: xpack/plugins/cloud/models.py:275 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:98 +#: xpack/plugins/cloud/models.py:282 msgid "Priority" msgstr "优先级" #: acls/models/base.py:38 assets/models/_user.py:51 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:98 -#: xpack/plugins/cloud/models.py:276 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:99 +#: xpack/plugins/cloud/models.py:283 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" #: acls/models/base.py:42 assets/models/cmd_filter.py:86 -#: authentication/serializers/connect_token_secret.py:90 +#: authentication/serializers/connect_token_secret.py:91 msgid "Reviewers" msgstr "审批人" #: acls/models/base.py:43 authentication/models/access_key.py:25 #: authentication/models/connection_token.py:53 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:81 terminal/models/session/sharing.py:29 +#: perms/models/asset_permission.py:82 terminal/models/session/sharing.py:29 #: tickets/const.py:37 msgid "Active" msgstr "激活中" @@ -1087,13 +1105,13 @@ msgid "Users" msgstr "用户管理" #: acls/models/base.py:98 assets/models/automations/base.py:17 -#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:377 +#: assets/models/cmd_filter.py:38 assets/serializers/asset/common.py:378 #: perms/serializers/user_permission.py:75 rbac/tree.py:35 msgid "Accounts" msgstr "账号管理" #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: ops/serializers/job.py:55 terminal/const.py:85 +#: ops/serializers/job.py:63 terminal/const.py:86 #: terminal/models/session/session.py:42 terminal/serializers/command.py:18 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -1102,12 +1120,13 @@ msgid "Command" msgstr "命令" #: acls/models/command_acl.py:17 assets/models/cmd_filter.py:59 -#: xpack/plugins/cloud/models.py:291 +#: xpack/plugins/cloud/models.py:299 msgid "Regex" msgstr "正则表达式" #: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 -#: settings/serializers/feature.py:17 xpack/plugins/license/models.py:30 +#: settings/models.py:182 settings/serializers/feature.py:19 +#: xpack/plugins/license/models.py:30 msgid "Content" msgstr "内容" @@ -1121,7 +1140,7 @@ msgstr "忽略大小写" #: acls/models/command_acl.py:33 acls/models/command_acl.py:97 #: acls/serializers/command_acl.py:29 -#: authentication/serializers/connect_token_secret.py:87 +#: authentication/serializers/connect_token_secret.py:88 #: terminal/templates/terminal/_msg_command_warning.html:14 msgid "Command group" msgstr "命令组" @@ -1206,7 +1225,7 @@ msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" #: acls/serializers/rules/rules.py:20 -#: xpack/plugins/cloud/serializers/task.py:137 +#: xpack/plugins/cloud/serializers/task.py:145 msgid "IP address invalid: `{}`" msgstr "IP 地址无效: `{}`" @@ -1275,7 +1294,7 @@ msgid "Applications" msgstr "应用管理" #: applications/models.py:16 xpack/plugins/cloud/models.py:37 -#: xpack/plugins/cloud/serializers/account.py:63 +#: xpack/plugins/cloud/serializers/account.py:67 msgid "Attrs" msgstr "属性" @@ -1287,7 +1306,7 @@ msgstr "应用程序" msgid "Can match application" msgstr "匹配应用" -#: assets/api/asset/asset.py:194 +#: assets/api/asset/asset.py:182 msgid "Cannot create asset directly, you should create a host or other" msgstr "不能直接创建资产, 你应该创建主机或其他资产" @@ -1315,11 +1334,11 @@ msgstr "同级别节点名字不能重复" msgid "App assets" msgstr "资产管理" -#: assets/automations/base/manager.py:133 +#: assets/automations/base/manager.py:188 msgid "{} disabled" msgstr "{} 已禁用" -#: assets/automations/base/manager.py:199 +#: assets/automations/base/manager.py:251 msgid " - Platform {} ansible disabled" msgstr " - 平台 {} Ansible 已禁用, 无法执行任务" @@ -1343,7 +1362,7 @@ msgid "Authentication failed" msgstr "认证失败" #: assets/automations/ping_gateway/manager.py:60 -#: assets/automations/ping_gateway/manager.py:86 terminal/const.py:101 +#: assets/automations/ping_gateway/manager.py:86 terminal/const.py:102 msgid "Connect failed" msgstr "连接失败" @@ -1374,10 +1393,10 @@ msgstr "收集资产信息" msgid "Disabled" msgstr "禁用" -#: assets/const/base.py:33 settings/serializers/basic.py:6 +#: assets/const/base.py:33 settings/serializers/basic.py:8 #: users/serializers/preference/koko.py:19 #: users/serializers/preference/lina.py:39 -#: users/serializers/preference/luna.py:69 +#: users/serializers/preference/luna.py:73 msgid "Basic" msgstr "基本" @@ -1388,7 +1407,7 @@ msgstr "脚本" #: assets/const/category.py:10 assets/models/asset/host.py:8 #: settings/serializers/auth/radius.py:16 settings/serializers/auth/sms.py:71 -#: settings/serializers/feature.py:47 terminal/models/component/endpoint.py:13 +#: settings/serializers/feature.py:49 terminal/models/component/endpoint.py:13 #: terminal/serializers/applet.py:17 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" @@ -1404,7 +1423,7 @@ msgstr "云服务" #: assets/const/category.py:14 assets/models/asset/gpt.py:11 #: assets/models/asset/web.py:16 audits/const.py:42 -#: terminal/models/applet/applet.py:27 users/const.py:52 +#: terminal/models/applet/applet.py:27 users/const.py:59 msgid "Web" msgstr "Web" @@ -1565,7 +1584,7 @@ msgstr "确认按钮选择器" msgid "API mode" msgstr "API 模式" -#: assets/const/types.py:224 +#: assets/const/types.py:247 msgid "All types" msgstr "所有类型" @@ -1590,19 +1609,19 @@ msgstr "SSH公钥" #: assets/models/_user.py:28 assets/models/automations/base.py:114 #: assets/models/cmd_filter.py:41 assets/models/group.py:19 #: audits/models.py:267 common/db/models.py:34 ops/models/base.py:54 -#: ops/models/job.py:230 users/models/user.py:1025 +#: ops/models/job.py:234 users/models/user.py:1041 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:29 assets/models/cmd_filter.py:42 -#: common/db/models.py:35 users/models/user.py:847 +#: common/db/models.py:35 users/models/user.py:854 msgid "Date updated" msgstr "更新日期" #: assets/models/_user.py:30 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:18 -#: common/db/models.py:32 users/models/user.py:836 -#: users/serializers/group.py:29 +#: common/db/models.py:32 users/models/user.py:843 +#: users/serializers/group.py:30 msgid "Created by" msgstr "创建者" @@ -1627,9 +1646,11 @@ msgid "Username same with user" msgstr "用户名与用户相同" #: assets/models/_user.py:52 authentication/models/connection_token.py:41 -#: authentication/serializers/connect_token_secret.py:113 -#: terminal/models/applet/applet.py:42 terminal/serializers/session.py:19 -#: terminal/serializers/session.py:45 terminal/serializers/storage.py:71 +#: authentication/serializers/connect_token_secret.py:114 +#: terminal/models/applet/applet.py:42 +#: terminal/models/virtualapp/virtualapp.py:24 +#: terminal/serializers/session.py:19 terminal/serializers/session.py:45 +#: terminal/serializers/storage.py:71 msgid "Protocol" msgstr "协议" @@ -1637,7 +1658,7 @@ msgstr "协议" msgid "Sudo" msgstr "Sudo" -#: assets/models/_user.py:55 ops/const.py:49 ops/const.py:59 +#: assets/models/_user.py:55 ops/const.py:50 ops/const.py:60 msgid "Shell" msgstr "Shell" @@ -1677,56 +1698,59 @@ msgstr "可以匹配系统用户" msgid "Cloud" msgstr "云服务" -#: assets/models/asset/common.py:92 assets/models/platform.py:16 +#: assets/models/asset/common.py:94 assets/models/platform.py:17 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:72 #: terminal/serializers/storage.py:133 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" msgstr "端口" -#: assets/models/asset/common.py:158 assets/serializers/asset/common.py:147 +#: assets/models/asset/common.py:160 assets/serializers/asset/common.py:147 msgid "Address" msgstr "地址" -#: assets/models/asset/common.py:159 assets/models/platform.py:119 +#: assets/models/asset/common.py:161 assets/models/platform.py:126 #: authentication/backends/passkey/models.py:12 -#: authentication/serializers/connect_token_secret.py:117 -#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:321 +#: authentication/serializers/connect_token_secret.py:118 +#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:329 msgid "Platform" msgstr "系统平台" -#: assets/models/asset/common.py:161 assets/models/domain.py:21 -#: authentication/serializers/connect_token_secret.py:135 -#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:323 +#: assets/models/asset/common.py:163 assets/models/domain.py:22 +#: authentication/serializers/connect_token_secret.py:136 +#: perms/serializers/user_permission.py:28 xpack/plugins/cloud/models.py:331 msgid "Domain" msgstr "网域" -#: assets/models/asset/common.py:165 -msgid "Labels" -msgstr "标签管理" +#: assets/models/asset/common.py:165 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/models/node.py:553 +#: perms/models/asset_permission.py:72 perms/serializers/permission.py:37 +#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:330 +msgid "Node" +msgstr "节点" -#: assets/models/asset/common.py:166 assets/serializers/asset/common.py:379 +#: assets/models/asset/common.py:167 assets/serializers/asset/common.py:380 #: assets/serializers/asset/host.py:11 msgid "Gathered info" msgstr "收集资产硬件信息" -#: assets/models/asset/common.py:167 assets/serializers/asset/custom.py:14 +#: assets/models/asset/common.py:168 assets/serializers/asset/custom.py:14 msgid "Custom info" msgstr "自定义属性" -#: assets/models/asset/common.py:345 +#: assets/models/asset/common.py:353 msgid "Can refresh asset hardware info" msgstr "可以更新资产硬件信息" -#: assets/models/asset/common.py:346 +#: assets/models/asset/common.py:354 msgid "Can test asset connectivity" msgstr "可以测试资产连接性" -#: assets/models/asset/common.py:347 +#: assets/models/asset/common.py:355 msgid "Can match asset" msgstr "可以匹配资产" -#: assets/models/asset/common.py:348 +#: assets/models/asset/common.py:356 msgid "Can change asset nodes" msgstr "可以修改资产节点" @@ -1750,11 +1774,11 @@ msgstr "客户端密钥" msgid "Allow invalid cert" msgstr "忽略证书校验" -#: assets/models/asset/gpt.py:8 +#: assets/models/asset/gpt.py:8 settings/serializers/feature.py:73 msgid "Proxy" msgstr "代理" -#: assets/models/automations/base.py:22 ops/models/job.py:226 +#: assets/models/automations/base.py:22 ops/models/job.py:230 #: settings/serializers/auth/sms.py:103 msgid "Parameters" msgstr "参数" @@ -1783,9 +1807,9 @@ msgstr "可连接性" msgid "Date verified" msgstr "校验日期" -#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:65 -#: perms/serializers/permission.py:32 users/models/group.py:25 -#: users/models/user.py:799 +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:66 +#: perms/serializers/permission.py:34 users/models/group.py:25 +#: users/models/user.py:806 msgid "User group" msgstr "用户组" @@ -1817,7 +1841,7 @@ msgstr "命令过滤规则" msgid "Favorite asset" msgstr "收藏的资产" -#: assets/models/gateway.py:34 assets/serializers/domain.py:16 +#: assets/models/gateway.py:34 assets/serializers/domain.py:17 msgid "Gateway" msgstr "网关" @@ -1825,7 +1849,7 @@ msgstr "网关" msgid "Asset group" msgstr "资产组" -#: assets/models/group.py:31 assets/models/platform.py:19 +#: assets/models/group.py:31 assets/models/platform.py:20 #: assets/serializers/platform.py:121 #: xpack/plugins/cloud/providers/nutanix.py:30 msgid "Default" @@ -1835,7 +1859,7 @@ msgstr "默认" msgid "Default asset group" msgstr "默认资产组" -#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1010 +#: assets/models/label.py:15 rbac/const.py:6 users/models/user.py:1026 msgid "System" msgstr "系统" @@ -1843,17 +1867,18 @@ msgstr "系统" #: assets/serializers/cagegory.py:11 assets/serializers/cagegory.py:18 #: assets/serializers/cagegory.py:24 #: authentication/models/connection_token.py:29 -#: authentication/serializers/connect_token_secret.py:124 -#: common/serializers/common.py:86 settings/models.py:33 +#: authentication/serializers/connect_token_secret.py:125 +#: common/serializers/common.py:86 labels/models.py:12 settings/models.py:34 #: users/models/preference.py:13 msgid "Value" msgstr "值" -#: assets/models/label.py:40 assets/serializers/asset/common.py:123 -#: assets/serializers/cagegory.py:10 assets/serializers/cagegory.py:17 -#: assets/serializers/cagegory.py:23 assets/serializers/platform.py:119 -#: authentication/serializers/connect_token_secret.py:123 -#: common/serializers/common.py:85 perms/serializers/user_permission.py:27 +#: assets/models/label.py:40 assets/serializers/cagegory.py:10 +#: assets/serializers/cagegory.py:17 assets/serializers/cagegory.py:23 +#: assets/serializers/platform.py:119 +#: authentication/serializers/connect_token_secret.py:124 +#: common/serializers/common.py:85 labels/models.py:17 labels/models.py:33 +#: labels/serializers.py:45 perms/serializers/user_permission.py:27 #: settings/serializers/msg.py:83 msgid "Label" msgstr "标签" @@ -1862,7 +1887,7 @@ msgstr "标签" msgid "New node" msgstr "新节点" -#: assets/models/node.py:467 audits/backends/db.py:55 audits/backends/db.py:56 +#: assets/models/node.py:467 audits/backends/db.py:65 audits/backends/db.py:66 msgid "empty" msgstr "空" @@ -1878,130 +1903,137 @@ msgstr "全称" msgid "Parent key" msgstr "ssh私钥" -#: assets/models/node.py:553 perms/serializers/permission.py:35 -#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:322 -msgid "Node" -msgstr "节点" - #: assets/models/node.py:556 msgid "Can match node" msgstr "可以匹配节点" -#: assets/models/platform.py:17 +#: assets/models/platform.py:18 msgid "Primary" msgstr "主要的" -#: assets/models/platform.py:18 +#: assets/models/platform.py:19 msgid "Required" msgstr "必须的" -#: assets/models/platform.py:20 +#: assets/models/platform.py:21 msgid "Public" msgstr "开放的" -#: assets/models/platform.py:21 assets/serializers/platform.py:49 +#: assets/models/platform.py:22 assets/serializers/platform.py:49 #: settings/serializers/settings.py:66 #: users/templates/users/reset_password.html:29 msgid "Setting" msgstr "设置" -#: assets/models/platform.py:38 audits/const.py:56 -#: authentication/backends/passkey/models.py:11 settings/models.py:36 +#: assets/models/platform.py:39 audits/const.py:56 +#: authentication/backends/passkey/models.py:11 settings/models.py:37 #: terminal/serializers/applet_host.py:33 msgid "Enabled" msgstr "启用" -#: assets/models/platform.py:39 +#: assets/models/platform.py:40 msgid "Ansible config" msgstr "Ansible 配置" -#: assets/models/platform.py:41 assets/serializers/platform.py:33 +#: assets/models/platform.py:42 assets/serializers/platform.py:33 msgid "Ping enabled" msgstr "启用资产探活" -#: assets/models/platform.py:42 assets/serializers/platform.py:34 +#: assets/models/platform.py:43 assets/serializers/platform.py:34 msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:43 +#: assets/models/platform.py:44 msgid "Ping params" msgstr "资产探活参数" -#: assets/models/platform.py:45 assets/models/platform.py:69 +#: assets/models/platform.py:46 assets/models/platform.py:70 #: assets/serializers/platform.py:35 msgid "Gather facts enabled" msgstr "启用收集资产信息" -#: assets/models/platform.py:47 assets/models/platform.py:71 +#: assets/models/platform.py:48 assets/models/platform.py:72 #: assets/serializers/platform.py:36 msgid "Gather facts method" msgstr "收集信息方式" -#: assets/models/platform.py:49 assets/models/platform.py:73 +#: assets/models/platform.py:50 assets/models/platform.py:74 msgid "Gather facts params" msgstr "收集信息参数" -#: assets/models/platform.py:51 assets/serializers/platform.py:39 +#: assets/models/platform.py:52 assets/serializers/platform.py:39 msgid "Change secret enabled" msgstr "启用改密" -#: assets/models/platform.py:53 assets/serializers/platform.py:40 +#: assets/models/platform.py:54 assets/serializers/platform.py:40 msgid "Change secret method" msgstr "改密方式" -#: assets/models/platform.py:55 +#: assets/models/platform.py:56 msgid "Change secret params" msgstr "改密参数" -#: assets/models/platform.py:57 assets/serializers/platform.py:41 +#: assets/models/platform.py:58 assets/serializers/platform.py:41 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:59 assets/serializers/platform.py:42 +#: assets/models/platform.py:60 assets/serializers/platform.py:42 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:61 +#: assets/models/platform.py:62 msgid "Push account params" msgstr "账号推送参数" -#: assets/models/platform.py:63 assets/serializers/platform.py:37 +#: assets/models/platform.py:64 assets/serializers/platform.py:37 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:65 assets/serializers/platform.py:38 +#: assets/models/platform.py:66 assets/serializers/platform.py:38 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:67 +#: assets/models/platform.py:68 msgid "Verify account params" msgstr "账号验证参数" -#: assets/models/platform.py:91 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:76 +msgid "Remove account enabled" +msgstr "开启账号移除" + +#: assets/models/platform.py:78 +msgid "Remove account method" +msgstr "账号移除方式" + +#: assets/models/platform.py:80 +msgid "Remove account params" +msgstr "账号移除参数" + +#: assets/models/platform.py:98 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:92 +#: assets/models/platform.py:99 labels/models.py:13 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:96 assets/serializers/platform.py:138 +#: assets/models/platform.py:103 assets/serializers/platform.py:138 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:98 assets/serializers/platform.py:166 +#: assets/models/platform.py:105 assets/serializers/platform.py:167 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:100 assets/serializers/platform.py:165 +#: assets/models/platform.py:107 assets/serializers/platform.py:166 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:101 assets/serializers/platform.py:144 +#: assets/models/platform.py:108 assets/serializers/platform.py:144 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:102 assets/serializers/platform.py:147 +#: assets/models/platform.py:109 assets/serializers/platform.py:147 msgid "Custom fields" msgstr "自定义属性" @@ -2017,11 +2049,11 @@ msgid "" msgstr "资产中批量更新平台,不符合平台类型跳过的资产" #: assets/serializers/asset/common.py:124 assets/serializers/platform.py:141 -#: authentication/serializers/connect_token_secret.py:29 -#: authentication/serializers/connect_token_secret.py:74 -#: perms/models/asset_permission.py:75 perms/serializers/permission.py:40 -#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:324 -#: xpack/plugins/cloud/serializers/task.py:31 +#: authentication/serializers/connect_token_secret.py:30 +#: authentication/serializers/connect_token_secret.py:75 +#: perms/models/asset_permission.py:76 perms/serializers/permission.py:42 +#: perms/serializers/user_permission.py:74 xpack/plugins/cloud/models.py:332 +#: xpack/plugins/cloud/serializers/task.py:33 msgid "Protocols" msgstr "协议组" @@ -2031,23 +2063,23 @@ msgid "Node path" msgstr "节点路径" #: assets/serializers/asset/common.py:145 -#: assets/serializers/asset/common.py:380 +#: assets/serializers/asset/common.py:381 msgid "Auto info" msgstr "自动化信息" -#: assets/serializers/asset/common.py:238 +#: assets/serializers/asset/common.py:239 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:274 +#: assets/serializers/asset/common.py:275 msgid "port out of range (0-65535)" msgstr "端口超出范围 (0-65535)" -#: assets/serializers/asset/common.py:281 +#: assets/serializers/asset/common.py:282 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" -#: assets/serializers/asset/common.py:309 +#: assets/serializers/asset/common.py:310 msgid "Invalid data" msgstr "无效的数据" @@ -2109,7 +2141,7 @@ msgid "Disk total" msgstr "硬盘大小" #: assets/serializers/asset/info/gathered.py:16 -#: authentication/serializers/connect_token_secret.py:114 +#: authentication/serializers/connect_token_secret.py:115 msgid "OS" msgstr "操作系统" @@ -2133,10 +2165,6 @@ msgstr "类型" msgid "This field must be unique." msgstr "字段必须唯一" -#: assets/serializers/label.py:12 -msgid "Assets amount" -msgstr "资产数量" - #: assets/serializers/node.py:17 msgid "value" msgstr "值" @@ -2189,11 +2217,11 @@ msgstr "选择" msgid "Automation" msgstr "自动化" -#: assets/serializers/platform.py:167 +#: assets/serializers/platform.py:168 msgid "Default Domain" msgstr "默认网域" -#: assets/serializers/platform.py:188 +#: assets/serializers/platform.py:189 msgid "type is required" msgstr "类型 该字段是必填项。" @@ -2271,11 +2299,11 @@ msgstr "没有匹配到资产,结束任务" msgid "Audits" msgstr "日志审计" -#: audits/backends/db.py:15 +#: audits/backends/db.py:16 msgid "The text content is too long. Use Elasticsearch to store operation logs" msgstr "文字内容太长。请使用 Elasticsearch 存储操作日志" -#: audits/backends/db.py:81 +#: audits/backends/db.py:91 msgid "Tips" msgstr "提示" @@ -2289,7 +2317,7 @@ msgstr "删除目录" #: audits/const.py:14 audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:238 +#: rbac/tree.py:240 msgid "Delete" msgstr "删除" @@ -2313,7 +2341,7 @@ msgstr "下载" msgid "Rename dir" msgstr "映射目录" -#: audits/const.py:23 rbac/tree.py:236 terminal/api/session/session.py:257 +#: audits/const.py:23 rbac/tree.py:238 terminal/api/session/session.py:257 #: terminal/templates/terminal/_msg_command_warning.html:18 #: terminal/templates/terminal/_msg_session_sharing.html:10 msgid "View" @@ -2321,7 +2349,7 @@ msgstr "查看" #: audits/const.py:26 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:235 +#: rbac/tree.py:237 msgid "Create" msgstr "创建" @@ -2351,8 +2379,9 @@ msgid "Close" msgstr "关闭" #: audits/const.py:43 settings/serializers/terminal.py:6 -#: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:164 -#: terminal/serializers/session.py:52 terminal/serializers/session.py:66 +#: terminal/models/applet/host.py:26 terminal/models/component/terminal.py:174 +#: terminal/models/virtualapp/provider.py:14 terminal/serializers/session.py:52 +#: terminal/serializers/session.py:66 msgid "Terminal" msgstr "终端" @@ -2419,12 +2448,12 @@ msgstr "会话" msgid "File transfer log" msgstr "文件管理" -#: audits/models.py:94 audits/serializers.py:89 +#: audits/models.py:94 audits/serializers.py:86 msgid "Resource Type" msgstr "资源类型" #: audits/models.py:95 audits/models.py:98 audits/models.py:144 -#: audits/serializers.py:88 +#: audits/serializers.py:85 labels/serializers.py:46 msgid "Resource" msgstr "资源" @@ -2468,7 +2497,7 @@ msgstr "登录 IP" #: audits/models.py:200 audits/serializers.py:52 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 -#: users/forms/profile.py:65 users/models/user.py:816 +#: users/forms/profile.py:65 users/models/user.py:823 #: users/serializers/profile.py:102 msgid "MFA" msgstr "MFA" @@ -2478,7 +2507,7 @@ msgid "Date login" msgstr "登录日期" #: audits/models.py:212 audits/models.py:266 audits/serializers.py:70 -#: audits/serializers.py:187 +#: audits/serializers.py:184 msgid "Authentication backend" msgstr "认证方式" @@ -2495,11 +2524,11 @@ msgid "User session" msgstr "用户会话" #: audits/models.py:308 -msgid "Offline ussr session" -msgstr "下限用户会话" +msgid "Offline user session" +msgstr "下线用户会话" #: audits/serializers.py:33 ops/models/adhoc.py:25 ops/models/base.py:16 -#: ops/models/base.py:53 ops/models/job.py:141 ops/models/job.py:229 +#: ops/models/base.py:53 ops/models/job.py:145 ops/models/job.py:233 #: ops/models/playbook.py:30 terminal/models/session/sharing.py:25 msgid "Creator" msgstr "创建者" @@ -2508,15 +2537,15 @@ msgstr "创建者" msgid "Reason display" msgstr "原因描述" -#: audits/serializers.py:137 +#: audits/serializers.py:134 #, python-format msgid "User %s %s this resource" msgstr "用户 %s %s 了当前资源" -#: audits/serializers.py:175 authentication/models/connection_token.py:47 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:79 +#: audits/serializers.py:172 authentication/models/connection_token.py:47 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:80 #: tickets/models/ticket/apply_application.py:31 -#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:834 +#: tickets/models/ticket/apply_asset.py:20 users/models/user.py:841 msgid "Date expired" msgstr "失效日期" @@ -2548,33 +2577,40 @@ msgid "Auth Token" msgstr "认证令牌" #: audits/signal_handlers/login_log.py:37 authentication/notifications.py:73 -#: authentication/views/login.py:77 authentication/views/wecom.py:160 -#: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 -#: users/models/user.py:746 users/models/user.py:848 +#: authentication/views/login.py:77 notifications/backends/__init__.py:11 +#: settings/serializers/auth/wecom.py:10 users/models/user.py:749 +#: users/models/user.py:855 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:123 +#: audits/signal_handlers/login_log.py:38 authentication/views/feishu.py:87 #: authentication/views/login.py:89 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 -#: settings/serializers/auth/feishu.py:13 users/models/user.py:748 -#: users/models/user.py:850 +#: settings/serializers/auth/feishu.py:13 users/models/user.py:751 +#: users/models/user.py:857 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers/login_log.py:39 authentication/views/dingtalk.py:160 +#: audits/signal_handlers/login_log.py:39 authentication/views/login.py:95 +#: authentication/views/slack.py:87 notifications/backends/__init__.py:15 +#: settings/serializers/auth/slack.py:10 users/models/user.py:752 +#: users/models/user.py:858 +msgid "Slack" +msgstr "" + +#: audits/signal_handlers/login_log.py:40 authentication/views/dingtalk.py:160 #: authentication/views/login.py:83 notifications/backends/__init__.py:12 -#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:747 -#: users/models/user.py:849 +#: settings/serializers/auth/dingtalk.py:10 users/models/user.py:750 +#: users/models/user.py:856 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers/login_log.py:40 +#: audits/signal_handlers/login_log.py:41 #: authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "临时密码" -#: audits/signal_handlers/login_log.py:41 authentication/views/login.py:95 +#: audits/signal_handlers/login_log.py:42 authentication/views/login.py:101 #: settings/serializers/auth/passkey.py:8 msgid "Passkey" msgstr "Passkey" @@ -2591,31 +2627,36 @@ msgstr "上传 FTP 文件到外部存储" msgid "Access keys can be created at most 10" msgstr "最多可以创建10个访问密钥" +#: authentication/api/common.py:34 settings/serializers/auth/sms.py:117 +#, python-format +msgid "The value in the parameter must contain %s" +msgstr "参数中的值必须包含 %s" + #: authentication/api/confirm.py:50 msgid "This action require verify your MFA" msgstr "该操作需要验证您的 MFA, 请先开启并配置" -#: authentication/api/connection_token.py:258 +#: authentication/api/connection_token.py:260 msgid "Reusable connection token is not allowed, global setting not enabled" msgstr "不允许使用可重复使用的连接令牌,未启用全局设置" -#: authentication/api/connection_token.py:372 +#: authentication/api/connection_token.py:374 msgid "Anonymous account is not supported for this asset" msgstr "匿名账号不支持当前资产" -#: authentication/api/connection_token.py:391 +#: authentication/api/connection_token.py:393 msgid "Account not found" msgstr "账号未找到" -#: authentication/api/connection_token.py:394 +#: authentication/api/connection_token.py:396 msgid "Permission expired" msgstr "授权已过期" -#: authentication/api/connection_token.py:424 +#: authentication/api/connection_token.py:426 msgid "ACL action is reject: {}({})" msgstr "ACL 动作是拒绝: {}({})" -#: authentication/api/connection_token.py:428 +#: authentication/api/connection_token.py:430 msgid "ACL action is review" msgstr "ACL 动作是复核" @@ -2656,20 +2697,20 @@ msgstr "认证" msgid "User invalid, disabled or expired" msgstr "用户无效,已禁用或已过期" -#: authentication/backends/drf.py:39 +#: authentication/backends/drf.py:50 msgid "Invalid token header. No credentials provided." msgstr "无效的令牌头。没有提供任何凭据。" -#: authentication/backends/drf.py:42 +#: authentication/backends/drf.py:53 msgid "Invalid token header. Sign string should not contain spaces." msgstr "无效的令牌头。符号字符串不应包含空格。" -#: authentication/backends/drf.py:48 +#: authentication/backends/drf.py:59 msgid "" "Invalid token header. Sign string should not contain invalid characters." msgstr "无效的令牌头。符号字符串不应包含无效字符。" -#: authentication/backends/drf.py:61 +#: authentication/backends/drf.py:72 msgid "Invalid token or cache refreshed." msgstr "刷新的令牌或缓存无效。" @@ -2829,29 +2870,33 @@ msgstr "手机号没有设置" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:62 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:59 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:204 -#: authentication/views/wecom.py:246 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:159 +#: authentication/views/wecom.py:201 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:211 -#: authentication/views/dingtalk.py:253 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:212 +#: authentication/views/dingtalk.py:254 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:168 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:128 msgid "FeiShu is not bound" msgstr "没有绑定飞书" -#: authentication/errors/mfa.py:38 +#: authentication/errors/mfa.py:38 authentication/views/slack.py:127 +msgid "Slack is not bound" +msgstr "Slack没有绑定" + +#: authentication/errors/mfa.py:43 msgid "Your password is invalid" msgstr "您的密码无效" -#: authentication/errors/mfa.py:43 +#: authentication/errors/mfa.py:48 #, python-format msgid "Please wait for %s seconds before retry" msgstr "请在 %s 秒后重试" @@ -2961,7 +3006,7 @@ msgstr "设置手机号码启用" msgid "Clear phone number to disable" msgstr "清空手机号码禁用" -#: authentication/middleware.py:94 settings/utils/ldap.py:679 +#: authentication/middleware.py:94 settings/utils/ldap.py:677 msgid "Authentication failed (before login check failed): {}" msgstr "认证失败 (登录前检查失败): {}" @@ -2984,7 +3029,7 @@ msgid "Please change your password" msgstr "请修改密码" #: authentication/models/access_key.py:22 -#: terminal/models/component/endpoint.py:95 +#: terminal/models/component/endpoint.py:96 msgid "IP group" msgstr "IPグループ" @@ -3023,7 +3068,7 @@ msgid "Reusable" msgstr "可以重复使用" #: authentication/models/connection_token.py:51 -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:83 msgid "From ticket" msgstr "来自工单" @@ -3055,11 +3100,11 @@ msgstr "没有用户或用户失效" msgid "No asset or inactive asset" msgstr "没有资产或资产未激活" -#: authentication/models/connection_token.py:265 +#: authentication/models/connection_token.py:269 msgid "Can view super connection token secret" msgstr "可以查看超级连接令牌密文" -#: authentication/models/connection_token.py:267 +#: authentication/models/connection_token.py:271 msgid "Super connection token" msgstr "超级连接令牌" @@ -3087,22 +3132,37 @@ msgstr "异地登录提醒" msgid "binding reminder" msgstr "绑定提醒" -#: authentication/serializers/connect_token_secret.py:115 +#: authentication/serializers/connect_token_secret.py:116 msgid "Is builtin" msgstr "内置的" -#: authentication/serializers/connect_token_secret.py:119 +#: authentication/serializers/connect_token_secret.py:120 msgid "Options" msgstr "选项" -#: authentication/serializers/connect_token_secret.py:126 +#: authentication/serializers/connect_token_secret.py:127 msgid "Component" msgstr "组件" -#: authentication/serializers/connect_token_secret.py:137 +#: authentication/serializers/connect_token_secret.py:138 msgid "Expired now" msgstr "立刻过期" +#: authentication/serializers/connect_token_secret.py:169 +#: terminal/models/virtualapp/virtualapp.py:25 +msgid "Image name" +msgstr "镜像名称" + +#: authentication/serializers/connect_token_secret.py:170 +#: terminal/models/virtualapp/virtualapp.py:27 +msgid "Image port" +msgstr "镜像端口" + +#: authentication/serializers/connect_token_secret.py:171 +#: terminal/models/virtualapp/virtualapp.py:26 +msgid "Image protocol" +msgstr "镜像协议" + #: authentication/serializers/connection_token.py:16 msgid "Expired time" msgstr "过期时间" @@ -3112,15 +3172,15 @@ msgid "Ticket info" msgstr "工单信息" #: authentication/serializers/connection_token.py:21 -#: perms/models/asset_permission.py:76 perms/serializers/permission.py:36 -#: perms/serializers/permission.py:57 +#: perms/models/asset_permission.py:77 perms/serializers/permission.py:38 +#: perms/serializers/permission.py:59 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "动作" #: authentication/serializers/connection_token.py:42 -#: perms/serializers/permission.py:38 perms/serializers/permission.py:58 +#: perms/serializers/permission.py:40 perms/serializers/permission.py:60 #: users/serializers/user.py:97 users/serializers/user.py:171 msgid "Is expired" msgstr "已过期" @@ -3134,8 +3194,8 @@ msgstr "{} 不能为空" msgid "Access IP" msgstr "IP 白名单" -#: authentication/serializers/token.py:92 perms/serializers/permission.py:37 -#: perms/serializers/permission.py:59 users/serializers/user.py:98 +#: authentication/serializers/token.py:92 perms/serializers/permission.py:39 +#: perms/serializers/permission.py:61 users/serializers/user.py:98 #: users/serializers/user.py:168 msgid "Is valid" msgstr "是否有效" @@ -3161,13 +3221,13 @@ msgid "Show" msgstr "显示" #: authentication/templates/authentication/_access_key_modal.html:66 -#: users/const.py:37 users/models/user.py:641 users/serializers/profile.py:92 +#: users/const.py:37 users/models/user.py:644 users/serializers/profile.py:92 #: users/templates/users/user_verify_mfa.html:36 msgid "Disable" msgstr "禁用" #: authentication/templates/authentication/_access_key_modal.html:67 -#: users/const.py:38 users/models/user.py:642 users/serializers/profile.py:93 +#: users/const.py:38 users/models/user.py:645 users/serializers/profile.py:93 #: users/templates/users/mfa_setting.html:26 #: users/templates/users/mfa_setting.html:68 msgid "Enable" @@ -3206,10 +3266,10 @@ msgstr "代码错误" #: authentication/templates/authentication/_msg_reset_password_code.html:9 #: authentication/templates/authentication/_msg_rest_password_success.html:2 #: authentication/templates/authentication/_msg_rest_public_key_success.html:2 -#: jumpserver/conf.py:449 +#: jumpserver/conf.py:455 #: perms/templates/perms/_msg_item_permissions_expire.html:3 #: perms/templates/perms/_msg_permed_items_expire.html:3 -#: tickets/templates/tickets/approve_check_password.html:33 +#: tickets/templates/tickets/approve_check_password.html:32 #: users/templates/users/_msg_account_expire_reminder.html:4 #: users/templates/users/_msg_password_expire_reminder.html:4 #: users/templates/users/_msg_reset_mfa.html:4 @@ -3367,25 +3427,40 @@ msgid "Do you want to retry ?" msgstr "是否重试 ?" #: authentication/utils.py:23 common/utils/ip/geoip/utils.py:24 -#: xpack/plugins/cloud/const.py:29 +#: xpack/plugins/cloud/const.py:32 msgid "LAN" msgstr "局域网" -#: authentication/views/base.py:67 +#: authentication/views/base.py:73 #: perms/templates/perms/_msg_permed_items_expire.html:21 msgid "If you have any question, please contact the administrator" msgstr "如果有疑问或需求,请联系系统管理员" +#: authentication/views/base.py:138 +#, python-format +msgid "%s query user failed" +msgstr "%s 查询用户失败" + +#: authentication/views/base.py:147 +#, python-format +msgid "The %s is already bound to another user" +msgstr "%s 已绑定到另一个用户" + +#: authentication/views/base.py:154 +#, python-format +msgid "Binding %s successfully" +msgstr "绑定 %s 成功" + #: authentication/views/dingtalk.py:42 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:210 +#: authentication/views/dingtalk.py:45 authentication/views/dingtalk.py:211 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:51 -#: authentication/views/wecom.py:58 +#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:47 +#: authentication/views/slack.py:47 authentication/views/wecom.py:55 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" @@ -3394,7 +3469,7 @@ msgstr "企业配置错误,请联系系统管理员" msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:129 authentication/views/wecom.py:130 +#: authentication/views/dingtalk.py:129 msgid "Invalid user_id" msgstr "无效的 user_id" @@ -3410,55 +3485,43 @@ msgstr "该钉钉已经绑定其他用户" msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:212 authentication/views/dingtalk.py:247 +#: authentication/views/dingtalk.py:213 authentication/views/dingtalk.py:248 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:254 +#: authentication/views/dingtalk.py:255 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:39 authentication/views/feishu.py:167 +#: authentication/views/feishu.py:35 authentication/views/feishu.py:127 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:67 +#: authentication/views/feishu.py:63 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:108 -msgid "FeiShu query user failed" -msgstr "飞书查询用户失败" - -#: authentication/views/feishu.py:117 -msgid "The FeiShu is already bound to another user" -msgstr "该飞书已经绑定其他用户" - -#: authentication/views/feishu.py:124 -msgid "Binding FeiShu successfully" -msgstr "绑定 飞书 成功" - -#: authentication/views/feishu.py:169 +#: authentication/views/feishu.py:129 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/login.py:210 +#: authentication/views/login.py:217 msgid "Redirecting" msgstr "跳转中" -#: authentication/views/login.py:211 +#: authentication/views/login.py:218 msgid "Redirecting to {} authentication" msgstr "正在跳转到 {} 认证" -#: authentication/views/login.py:234 +#: authentication/views/login.py:241 msgid "Login timeout, please try again." msgstr "登录超时,请重新登录" -#: authentication/views/login.py:277 +#: authentication/views/login.py:284 msgid "User email already exists ({})" msgstr "用户邮箱已存在 ({})" -#: authentication/views/login.py:355 +#: authentication/views/login.py:362 msgid "" "Wait for {} confirm, You also can copy link to her/him
\n" " Don't close this page" @@ -3466,43 +3529,47 @@ msgstr "" "等待 {} 确认, 你也可以复制链接发给他/她
\n" " 不要关闭本页面" -#: authentication/views/login.py:360 +#: authentication/views/login.py:367 msgid "No ticket found" msgstr "没有发现工单" -#: authentication/views/login.py:396 +#: authentication/views/login.py:403 msgid "Logout success" msgstr "退出登录成功" -#: authentication/views/login.py:397 +#: authentication/views/login.py:404 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:43 +#: authentication/views/slack.py:35 authentication/views/slack.py:126 +msgid "Slack Error" +msgstr "Slack 错误" + +#: authentication/views/slack.py:63 +msgid "Slack is already bound" +msgstr "Slack 已经绑定" + +#: authentication/views/slack.py:128 +msgid "Failed to get user from Slack" +msgstr "从 Slack 获取用户失败" + +#: authentication/views/wecom.py:40 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:46 authentication/views/wecom.py:203 +#: authentication/views/wecom.py:43 authentication/views/wecom.py:158 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:145 -msgid "WeCom query user failed" -msgstr "企业微信查询用户失败" +#: authentication/views/wecom.py:118 +msgid "Wecom" +msgstr "" -#: authentication/views/wecom.py:154 -msgid "The WeCom is already bound to another user" -msgstr "该企业微信已经绑定其他用户" - -#: authentication/views/wecom.py:161 -msgid "Binding WeCom successfully" -msgstr "绑定 企业微信 成功" - -#: authentication/views/wecom.py:205 authentication/views/wecom.py:240 +#: authentication/views/wecom.py:160 authentication/views/wecom.py:195 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:247 +#: authentication/views/wecom.py:202 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" @@ -3522,12 +3589,12 @@ msgstr "定时触发" msgid "Ready" msgstr "准备" -#: common/const/choices.py:16 terminal/const.py:76 tickets/const.py:29 +#: common/const/choices.py:16 terminal/const.py:77 tickets/const.py:29 #: tickets/const.py:39 msgid "Pending" msgstr "待定的" -#: common/const/choices.py:17 ops/const.py:71 +#: common/const/choices.py:17 ops/const.py:72 msgid "Running" msgstr "运行中" @@ -3596,10 +3663,10 @@ msgid "Invalid ids for ids, should be a list" msgstr "无效的ID,应为列表" #: common/db/fields.py:585 common/db/fields.py:590 -#: common/serializers/fields.py:104 tickets/serializers/ticket/common.py:58 +#: common/serializers/fields.py:134 tickets/serializers/ticket/common.py:58 #: xpack/plugins/cloud/serializers/account_attrs.py:56 #: xpack/plugins/cloud/serializers/account_attrs.py:79 -#: xpack/plugins/cloud/serializers/account_attrs.py:143 +#: xpack/plugins/cloud/serializers/account_attrs.py:150 msgid "This field is required." msgstr "该字段是必填项。" @@ -3619,7 +3686,7 @@ msgstr "忽略的" msgid "discard time" msgstr "忽略时间" -#: common/db/models.py:33 users/models/user.py:837 +#: common/db/models.py:33 users/models/user.py:844 msgid "Updated by" msgstr "最后更新者" @@ -3639,7 +3706,7 @@ msgstr "组织 ID" msgid "The file content overflowed (The maximum length `{}` bytes)" msgstr "文件内容太大 (最大长度 `{}` 字节)" -#: common/drf/parsers/base.py:195 +#: common/drf/parsers/base.py:199 msgid "Parse file error: {}" msgstr "解析文件错误: {}" @@ -3647,7 +3714,7 @@ msgstr "解析文件错误: {}" msgid "Invalid excel file" msgstr "无效的 excel 文件" -#: common/drf/renders/base.py:206 +#: common/drf/renders/base.py:207 msgid "" "{} - The encryption password has not been set - please go to personal " "information -> file encryption password to set the encryption password" @@ -3694,6 +3761,10 @@ msgstr "不支持 Elasticsearch8" msgid "Network error, please contact system administrator" msgstr "网络错误,请联系系统管理员" +#: common/sdk/im/slack/__init__.py:79 +msgid "Unknown error occur" +msgstr "发生未知错误" + #: common/sdk/im/wecom/__init__.py:16 msgid "WeCom error, please contact system administrator" msgstr "企业微信错误,请联系系统管理员" @@ -3735,6 +3806,10 @@ msgstr "华为云" msgid "CMPP v2.0" msgstr "CMPP v2.0" +#: common/sdk/sms/endpoint.py:21 +msgid "Custom type (File)" +msgstr "自定义 (文件)" + #: common/sdk/sms/endpoint.py:32 msgid "SMS provider not support: {}" msgstr "短信服务商不支持:{}" @@ -3759,24 +3834,30 @@ msgstr "请在 {} 秒后发送" msgid "Children" msgstr "节点" -#: common/serializers/fields.py:105 +#: common/serializers/fields.py:135 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "错误的 pk \"{pk_value}\" - 对象不存在" -#: common/serializers/fields.py:106 +#: common/serializers/fields.py:136 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "错误类型。期望 pk 值,收到 {data_type}。" -#: common/serializers/fields.py:180 +#: common/serializers/fields.py:210 msgid "Invalid data type, should be list" msgstr "错误的数据类型,应该是列表" -#: common/serializers/fields.py:195 +#: common/serializers/fields.py:225 msgid "Invalid choice: {}" msgstr "无效选项: {}" +#: common/serializers/mixin.py:397 labels/apps.py:8 +msgid "Labels" +msgstr "标签管理" + +# msgid "Labels" +# msgstr "标签管理" #: common/tasks.py:21 common/utils/verify_code.py:16 msgid "Send email" msgstr "发件邮件" @@ -3814,16 +3895,16 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" -#: jumpserver/conf.py:444 +#: jumpserver/conf.py:450 #, python-brace-format msgid "The verification code is: {code}" msgstr "验证码为: {code}" -#: jumpserver/conf.py:448 +#: jumpserver/conf.py:454 msgid "Create account successfully" msgstr "创建账号成功" -#: jumpserver/conf.py:450 +#: jumpserver/conf.py:456 msgid "Your account has been created successfully" msgstr "你的账号已创建成功" @@ -3858,6 +3939,26 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" +#: labels/models.py:36 +msgid "Resource ID" +msgstr "资源 ID" + +#: labels/models.py:41 +msgid "Labeled resource" +msgstr "关联的资源" + +#: labels/serializers.py:22 +msgid "Resource count" +msgstr "资源数量" + +#: labels/serializers.py:28 +msgid "Cannot contain \":,\"" +msgstr "不能包含\":,\"" + +#: labels/serializers.py:43 +msgid "Resource type" +msgstr "资源类型" + #: notifications/backends/__init__.py:13 msgid "Site message" msgstr "站内信" @@ -3882,30 +3983,40 @@ msgstr "系统信息" msgid "Publish the station message" msgstr "发布站内消息" -#: ops/ansible/inventory.py:95 ops/models/job.py:61 +#: ops/ansible/inventory.py:97 ops/models/job.py:62 msgid "No account available" msgstr "无可用账号" -#: ops/ansible/inventory.py:259 +#: ops/ansible/inventory.py:264 msgid "Ansible disabled" msgstr "Ansible 已禁用" -#: ops/ansible/inventory.py:275 +#: ops/ansible/inventory.py:280 msgid "Skip hosts below:" msgstr "跳过以下主机: " -#: ops/api/celery.py:64 ops/api/celery.py:79 +#: ops/api/celery.py:65 ops/api/celery.py:80 msgid "Waiting task start" msgstr "等待任务开始" -#: ops/api/celery.py:162 +#: ops/api/celery.py:203 msgid "Task {} not found" msgstr "任务 {} 不存在" -#: ops/api/celery.py:167 +#: ops/api/celery.py:208 msgid "Task {} args or kwargs error" msgstr "任务 {} 执行参数错误" +#: ops/api/job.py:132 +msgid "Duplicate file exists" +msgstr "存在同名文件" + +#: ops/api/job.py:137 +#, python-brace-format +msgid "" +"File size exceeds maximum limit. Please select a file smaller than {limit}MB" +msgstr "文件大小超过最大限制。请选择小于 {limit}MB 的文件。" + #: ops/api/playbook.py:39 msgid "Currently playbook is being used in a job" msgstr "当前 playbook 正在作业中使用" @@ -3934,7 +4045,7 @@ msgstr "文件密钥该字段是必填项。" msgid "This file can not be delete" msgstr "无法删除此文件" -#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:17 rbac/tree.py:57 msgid "App ops" msgstr "作业中心" @@ -3974,51 +4085,55 @@ msgstr "VCS" msgid "Adhoc" msgstr "命令" -#: ops/const.py:39 ops/models/job.py:138 +#: ops/const.py:39 ops/models/job.py:143 msgid "Playbook" msgstr "Playbook" -#: ops/const.py:43 +#: ops/const.py:40 +msgid "Upload File" +msgstr "上传" + +#: ops/const.py:44 msgid "Privileged Only" msgstr "仅限特权账号" -#: ops/const.py:44 +#: ops/const.py:45 msgid "Privileged First" msgstr "特权账号优先" -#: ops/const.py:50 ops/const.py:60 +#: ops/const.py:51 ops/const.py:61 msgid "Powershell" msgstr "PowerShell" -#: ops/const.py:51 ops/const.py:61 +#: ops/const.py:52 ops/const.py:62 msgid "Python" msgstr "Python" -#: ops/const.py:52 ops/const.py:62 +#: ops/const.py:53 ops/const.py:63 msgid "MySQL" msgstr "MySQL" -#: ops/const.py:53 ops/const.py:64 +#: ops/const.py:54 ops/const.py:65 msgid "PostgreSQL" msgstr "PostgreSQL" -#: ops/const.py:54 ops/const.py:65 +#: ops/const.py:55 ops/const.py:66 msgid "SQLServer" msgstr "SQLServer" -#: ops/const.py:55 ops/const.py:67 +#: ops/const.py:56 ops/const.py:68 msgid "Raw" msgstr "Raw" -#: ops/const.py:63 +#: ops/const.py:64 msgid "MariaDB" msgstr "MariaDB" -#: ops/const.py:66 +#: ops/const.py:67 msgid "Oracle" msgstr "Oracle" -#: ops/const.py:73 +#: ops/const.py:74 msgid "Timeout" msgstr "超时" @@ -4055,15 +4170,17 @@ msgstr "需要周期或定期设置" msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:23 ops/models/job.py:133 +#: ops/models/adhoc.py:23 ops/models/job.py:140 msgid "Module" msgstr "模块" -#: ops/models/adhoc.py:24 ops/models/celery.py:58 ops/models/job.py:131 +#: ops/models/adhoc.py:24 ops/models/celery.py:58 ops/models/job.py:138 #: terminal/models/component/task.py:14 msgid "Args" msgstr "参数" +# msgid "Creator" +# msgstr "创建者" #: ops/models/base.py:19 msgid "Account policy" msgstr "账号策略" @@ -4072,16 +4189,16 @@ msgstr "账号策略" msgid "Last execution" msgstr "最后执行" -#: ops/models/base.py:22 ops/serializers/job.py:19 +#: ops/models/base.py:22 ops/serializers/job.py:17 msgid "Date last run" msgstr "最后运行日期" -#: ops/models/base.py:51 ops/models/job.py:227 -#: xpack/plugins/cloud/models.py:199 +#: ops/models/base.py:51 ops/models/job.py:231 +#: xpack/plugins/cloud/models.py:202 msgid "Result" msgstr "结果" -#: ops/models/base.py:52 ops/models/job.py:228 +#: ops/models/base.py:52 ops/models/job.py:232 msgid "Summary" msgstr "汇总" @@ -4114,43 +4231,43 @@ msgstr "发布日期" msgid "Celery Task Execution" msgstr "Celery 任务执行" -#: ops/models/job.py:135 +#: ops/models/job.py:141 msgid "Chdir" msgstr "运行目录" -#: ops/models/job.py:136 +#: ops/models/job.py:142 msgid "Timeout (Seconds)" msgstr "超时时间 (秒)" -#: ops/models/job.py:143 +#: ops/models/job.py:147 msgid "Use Parameter Define" msgstr "使用参数定义" -#: ops/models/job.py:144 +#: ops/models/job.py:148 msgid "Parameters define" msgstr "参数定义" -#: ops/models/job.py:145 +#: ops/models/job.py:149 msgid "Runas" msgstr "运行用户" -#: ops/models/job.py:147 +#: ops/models/job.py:151 msgid "Runas policy" msgstr "用户策略" -#: ops/models/job.py:211 +#: ops/models/job.py:215 msgid "Job" msgstr "作业" -#: ops/models/job.py:234 +#: ops/models/job.py:238 msgid "Material" msgstr "Material" -#: ops/models/job.py:236 +#: ops/models/job.py:240 msgid "Material Type" msgstr "Material 类型" -#: ops/models/job.py:536 +#: ops/models/job.py:557 msgid "Job Execution" msgstr "作业执行" @@ -4190,19 +4307,19 @@ msgstr "内存使用率超过 {max_threshold}%: => {value}" msgid "CPU load more than {max_threshold}: => {value}" msgstr "CPU 使用率超过 {max_threshold}: => {value}" -#: ops/serializers/job.py:17 +#: ops/serializers/job.py:15 msgid "Run after save" msgstr "保存后执行" -#: ops/serializers/job.py:54 +#: ops/serializers/job.py:62 msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:57 terminal/serializers/session.py:53 +#: ops/serializers/job.py:65 terminal/serializers/session.py:53 msgid "Is finished" msgstr "是否完成" -#: ops/serializers/job.py:58 +#: ops/serializers/job.py:66 msgid "Time cost" msgstr "花费时间" @@ -4292,7 +4409,7 @@ msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进 msgid "The organization have resource ({}) cannot be deleted" msgstr "组织存在资源 ({}) 不能被删除" -#: orgs/apps.py:7 rbac/tree.py:126 +#: orgs/apps.py:7 rbac/tree.py:128 msgid "App organizations" msgstr "组织管理" @@ -4321,7 +4438,8 @@ msgstr "默认组织" msgid "SYSTEM" msgstr "系统组织" -#: orgs/models.py:83 rbac/models/role.py:36 terminal/models/applet/applet.py:41 +#: orgs/models.py:83 rbac/models/role.py:36 settings/models.py:183 +#: terminal/models/applet/applet.py:41 msgid "Builtin" msgstr "内置的" @@ -4381,7 +4499,7 @@ msgstr "文件传输" msgid "Clipboard" msgstr "剪贴板" -#: perms/models/asset_permission.py:88 +#: perms/models/asset_permission.py:89 msgid "Asset permission" msgstr "资产授权" @@ -4417,6 +4535,11 @@ msgstr "授权账号" msgid "today" msgstr "今天" +#: perms/notifications.py:12 perms/notifications.py:44 +#: settings/serializers/feature.py:106 +msgid "day" +msgstr "天" + #: perms/notifications.py:15 msgid "You permed assets is about to expire" msgstr "你授权的资产即将到期" @@ -4446,11 +4569,11 @@ msgstr "发送资产权限过期通知" #, python-format msgid "" "\n" -" The following %(item_type)s will expire in %(count)s days\n" +" The following %(item_type)s will expire in %(count)s\n" " " msgstr "" "\n" -" 以下 %(item_type)s 即将在 %(count)s 天后过期\n" +" 以下 %(item_type)s 即将在 %(count)s 后过期\n" " " #: rbac/api/role.py:35 @@ -4461,7 +4584,7 @@ msgstr "内部角色,不能删除" msgid "The role has been bound to users, can't be destroy" msgstr "角色已绑定用户,不能删除" -#: rbac/api/role.py:87 +#: rbac/api/role.py:100 msgid "Internal role, can't be update" msgstr "内部角色,不能更新" @@ -4525,7 +4648,7 @@ msgstr "文件管理" msgid "Can view System Tools" msgstr "可以查看系统工具" -#: rbac/models/permission.py:27 rbac/models/role.py:34 +#: rbac/models/permission.py:78 rbac/models/role.py:34 msgid "Permissions" msgstr "授权" @@ -4535,7 +4658,7 @@ msgid "Scope" msgstr "范围" #: rbac/models/role.py:46 rbac/models/rolebinding.py:52 -#: users/models/user.py:803 +#: users/models/user.py:810 msgid "Role" msgstr "角色" @@ -4573,11 +4696,12 @@ msgstr "系统角色绑定" msgid "Perms" msgstr "权限" -#: rbac/serializers/role.py:27 users/serializers/group.py:30 +#: rbac/serializers/role.py:27 users/serializers/group.py:31 msgid "Users amount" msgstr "用户数量" #: rbac/serializers/role.py:28 terminal/models/applet/applet.py:34 +#: terminal/models/virtualapp/virtualapp.py:20 msgid "Display name" msgstr "显示名称" @@ -4601,7 +4725,7 @@ msgstr "工作台" msgid "Audit view" msgstr "审计台" -#: rbac/tree.py:27 settings/models.py:158 +#: rbac/tree.py:27 settings/models.py:159 msgid "System setting" msgstr "系统设置" @@ -4609,59 +4733,64 @@ msgstr "系统设置" msgid "Session audits" msgstr "会话审计" -#: rbac/tree.py:47 +#: rbac/tree.py:49 msgid "Cloud import" msgstr "云同步" -#: rbac/tree.py:48 +#: rbac/tree.py:50 msgid "Backup account" msgstr "备份账号" -#: rbac/tree.py:49 +#: rbac/tree.py:51 msgid "Gather account" msgstr "收集账号" -#: rbac/tree.py:51 +#: rbac/tree.py:53 msgid "Asset change auth" msgstr "资产改密" -#: rbac/tree.py:52 +#: rbac/tree.py:54 msgid "Terminal setting" msgstr "终端设置" -#: rbac/tree.py:53 +#: rbac/tree.py:55 msgid "Task Center" msgstr "任务中心" -#: rbac/tree.py:54 +#: rbac/tree.py:56 msgid "My assets" msgstr "我的资产" -#: rbac/tree.py:56 terminal/models/applet/applet.py:52 -#: terminal/models/applet/applet.py:315 terminal/models/applet/host.py:30 +#: rbac/tree.py:58 terminal/models/applet/applet.py:52 +#: terminal/models/applet/applet.py:316 terminal/models/applet/host.py:30 #: terminal/serializers/applet.py:15 msgid "Applet" msgstr "远程应用" -#: rbac/tree.py:127 +#: rbac/tree.py:129 msgid "Ticket comment" msgstr "工单评论" -#: rbac/tree.py:128 settings/serializers/feature.py:58 +#: rbac/tree.py:130 settings/serializers/feature.py:98 #: tickets/models/ticket/general.py:307 msgid "Ticket" msgstr "工单管理" -#: rbac/tree.py:129 +#: rbac/tree.py:131 msgid "Common setting" msgstr "一般设置" -#: rbac/tree.py:130 +#: rbac/tree.py:132 msgid "View permission tree" msgstr "查看授权树" -#: settings/api/dingtalk.py:31 settings/api/feishu.py:36 -#: settings/api/sms.py:160 settings/api/vault.py:40 settings/api/wecom.py:37 +#: settings/api/chat.py:40 +msgid "Chat AI is not enabled" +msgstr "聊天 AI 没有开启" + +#: settings/api/chat.py:79 settings/api/dingtalk.py:31 +#: settings/api/feishu.py:36 settings/api/slack.py:34 settings/api/sms.py:160 +#: settings/api/vault.py:40 settings/api/wecom.py:37 msgid "Test success" msgstr "测试成功" @@ -4669,23 +4798,16 @@ msgstr "测试成功" msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: settings/api/ldap.py:176 -msgid "Synchronization start, please wait." -msgstr "同步开始,请稍等" +#: settings/api/ldap.py:101 +msgid "" +"Users are not synchronized, please click the user synchronization button" +msgstr "用户未同步,请点击同步用户按钮" -#: settings/api/ldap.py:180 -msgid "Synchronization is running, please wait." -msgstr "同步正在运行,请稍等" - -#: settings/api/ldap.py:185 -msgid "Synchronization error: {}" -msgstr "同步错误: {}" - -#: settings/api/ldap.py:223 +#: settings/api/ldap.py:137 msgid "Get ldap users is None" msgstr "获取 LDAP 用户为 None" -#: settings/api/ldap.py:233 +#: settings/api/ldap.py:147 msgid "Imported {} users successfully (Organization: {})" msgstr "成功导入 {} 个用户 ( 组织: {} )" @@ -4701,66 +4823,78 @@ msgstr "测试手机号 该字段是必填项。" msgid "Settings" msgstr "系统设置" -#: settings/models.py:35 users/models/preference.py:14 +#: settings/models.py:36 users/models/preference.py:14 msgid "Encrypted" msgstr "加密的" -#: settings/models.py:160 +#: settings/models.py:161 msgid "Can change email setting" msgstr "邮件设置" -#: settings/models.py:161 +#: settings/models.py:162 msgid "Can change auth setting" msgstr "认证设置" -#: settings/models.py:162 +#: settings/models.py:163 msgid "Can change auth ops" msgstr "任务中心设置" -#: settings/models.py:163 +#: settings/models.py:164 msgid "Can change auth ticket" msgstr "工单设置" -#: settings/models.py:164 +#: settings/models.py:165 +msgid "Can change virtual app setting" +msgstr "可以更改虚拟应用设置" + +#: settings/models.py:166 msgid "Can change auth announcement" msgstr "公告设置" -#: settings/models.py:165 +#: settings/models.py:167 msgid "Can change vault setting" msgstr "可以更改 vault 设置" -#: settings/models.py:166 +#: settings/models.py:168 +msgid "Can change chat ai setting" +msgstr "可以修改聊天 AI 设置" + +#: settings/models.py:169 msgid "Can change system msg sub setting" msgstr "消息订阅设置" -#: settings/models.py:167 +#: settings/models.py:170 msgid "Can change sms setting" msgstr "短信设置" -#: settings/models.py:168 +#: settings/models.py:171 msgid "Can change security setting" msgstr "安全设置" -#: settings/models.py:169 +#: settings/models.py:172 msgid "Can change clean setting" msgstr "定期清理" -#: settings/models.py:170 +#: settings/models.py:173 msgid "Can change interface setting" msgstr "界面设置" -#: settings/models.py:171 +#: settings/models.py:174 msgid "Can change license setting" msgstr "许可证设置" -#: settings/models.py:172 +#: settings/models.py:175 msgid "Can change terminal setting" msgstr "终端设置" -#: settings/models.py:173 +#: settings/models.py:176 msgid "Can change other setting" msgstr "其它设置" +#: settings/models.py:186 +msgid "Chat prompt" +msgstr "聊天提示" + #: settings/serializers/auth/base.py:12 msgid "LDAP Auth" msgstr "LDAP 认证" @@ -4794,22 +4928,26 @@ msgid "FeiShu Auth" msgstr "飞书 认证" #: settings/serializers/auth/base.py:20 +msgid "Slack Auth" +msgstr "Slack 认证" + +#: settings/serializers/auth/base.py:21 msgid "WeCom Auth" msgstr "企业微信 认证" -#: settings/serializers/auth/base.py:21 +#: settings/serializers/auth/base.py:22 msgid "SSO Auth" msgstr "SSO 令牌认证" -#: settings/serializers/auth/base.py:22 +#: settings/serializers/auth/base.py:23 msgid "Passkey Auth" msgstr "Passkey 认证" -#: settings/serializers/auth/base.py:25 +#: settings/serializers/auth/base.py:26 msgid "Forgot password url" msgstr "忘记密码 URL" -#: settings/serializers/auth/base.py:28 +#: settings/serializers/auth/base.py:29 msgid "Enable login redirect msg" msgstr "启用登录跳转提示" @@ -5110,6 +5248,10 @@ msgstr "SP 密钥" msgid "SP cert" msgstr "SP 证书" +#: settings/serializers/auth/slack.py:12 +msgid "Enable Slack Auth" +msgstr "启用 Slack 认证" + #: settings/serializers/auth/sms.py:17 msgid "Enable SMS" msgstr "启用 SMS" @@ -5188,11 +5330,6 @@ msgstr "URL" msgid "Request method" msgstr "请求方式" -#: settings/serializers/auth/sms.py:117 -#, python-format -msgid "The value in the parameter must contain %s" -msgstr "参数中的值必须包含 %s" - #: settings/serializers/auth/sso.py:16 msgid "Enable SSO auth" msgstr "启用 SSO 令牌认证" @@ -5206,7 +5343,7 @@ msgid "SSO auth key TTL" msgstr "令牌有效期" #: settings/serializers/auth/sso.py:20 -#: xpack/plugins/cloud/serializers/account_attrs.py:193 +#: xpack/plugins/cloud/serializers/account_attrs.py:200 msgid "Unit: second" msgstr "单位: 秒" @@ -5214,11 +5351,11 @@ msgstr "单位: 秒" msgid "Enable WeCom Auth" msgstr "启用企业微信认证" -#: settings/serializers/basic.py:9 +#: settings/serializers/basic.py:11 msgid "Site url" msgstr "当前站点 URL" -#: settings/serializers/basic.py:11 +#: settings/serializers/basic.py:13 msgid "" "External URL, email links or other system callbacks are used to access it, " "eg: http://dev.jumpserver.org:8080" @@ -5226,38 +5363,42 @@ msgstr "" "外部可访问的 URL, 用于邮件链接或其它系统回调, 例如: http://dev.jumpserver." "org:8080" -#: settings/serializers/basic.py:16 +#: settings/serializers/basic.py:18 msgid "User guide url" msgstr "用户向导URL" -#: settings/serializers/basic.py:17 +#: settings/serializers/basic.py:19 msgid "User first login update profile done redirect to it" msgstr "用户第一次登录,修改profile后重定向到地址, 可以是 wiki 或 其他说明文档" -#: settings/serializers/basic.py:20 +#: settings/serializers/basic.py:22 msgid "Global organization name" msgstr "全局组织名" -#: settings/serializers/basic.py:21 +#: settings/serializers/basic.py:23 msgid "The name of global organization to display" msgstr "全局组织的显示名称,默认为 全局组织" -#: settings/serializers/basic.py:24 +#: settings/serializers/basic.py:26 msgid "Help Docs URL" msgstr "文档链接" -#: settings/serializers/basic.py:25 +#: settings/serializers/basic.py:27 msgid "default: http://docs.jumpserver.org" msgstr "默认: http://dev.jumpserver.org:8080" -#: settings/serializers/basic.py:28 +#: settings/serializers/basic.py:30 msgid "Help Support URL" msgstr "支持链接" -#: settings/serializers/basic.py:29 +#: settings/serializers/basic.py:31 msgid "default: http://www.jumpserver.org/support/" msgstr "默认: http://www.jumpserver.org/support/" +#: settings/serializers/basic.py:44 +msgid "Organization name already exists" +msgstr "组织名称已存在" + #: settings/serializers/cleaning.py:11 msgid "Period clean" msgstr "定時清掃" @@ -5301,70 +5442,98 @@ msgid "" msgstr "" "会话、录像,命令记录超过该时长将会被清除 (影响数据库存储,OSS 等不受影响)" -#: settings/serializers/feature.py:16 +#: settings/serializers/feature.py:18 msgid "Subject" msgstr "主题" -#: settings/serializers/feature.py:20 +#: settings/serializers/feature.py:22 msgid "More url" msgstr "更多信息 URL" -#: settings/serializers/feature.py:34 settings/serializers/feature.py:37 +#: settings/serializers/feature.py:36 settings/serializers/feature.py:39 msgid "Announcement" msgstr "公告" -#: settings/serializers/feature.py:36 +#: settings/serializers/feature.py:38 msgid "Enable announcement" msgstr "启用公告" -#: settings/serializers/feature.py:44 +#: settings/serializers/feature.py:46 msgid "Enable Vault" msgstr "启用 Vault" -#: settings/serializers/feature.py:53 +#: settings/serializers/feature.py:55 msgid "Mount Point" msgstr "挂在点" #: settings/serializers/feature.py:60 +msgid "Chat AI" +msgstr "聊天 AI" + +#: settings/serializers/feature.py:64 +msgid "Enable Chat AI" +msgstr "启动聊天 AI" + +#: settings/serializers/feature.py:67 +msgid "Base Url" +msgstr "基本地址" + +#: settings/serializers/feature.py:70 templates/_header_bar.html:90 +msgid "API Key" +msgstr "API Key" + +#: settings/serializers/feature.py:76 +msgid "GPT Model" +msgstr "GPT 模型" + +#: settings/serializers/feature.py:100 msgid "Enable tickets" msgstr "启用工单" -#: settings/serializers/feature.py:63 +#: settings/serializers/feature.py:103 msgid "Ticket authorize default time" msgstr "默认工单授权时间" -#: settings/serializers/feature.py:66 -msgid "day" -msgstr "天" - -#: settings/serializers/feature.py:66 +#: settings/serializers/feature.py:106 msgid "hour" msgstr "时" -#: settings/serializers/feature.py:67 +#: settings/serializers/feature.py:107 msgid "Ticket authorize default time unit" msgstr "默认工单授权时间单位" -#: settings/serializers/feature.py:72 +#: settings/serializers/feature.py:112 msgid "Feature" msgstr "功能" -#: settings/serializers/feature.py:75 +#: settings/serializers/feature.py:115 msgid "Operation center" msgstr "作业中心" -#: settings/serializers/feature.py:76 +#: settings/serializers/feature.py:116 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/feature.py:80 +#: settings/serializers/feature.py:120 msgid "Operation center command blacklist" msgstr "作业中心命令黑名单" -#: settings/serializers/feature.py:81 +#: settings/serializers/feature.py:121 msgid "Commands that are not allowed execute." msgstr "不允许执行的命令" +#: settings/serializers/feature.py:126 +#: terminal/models/virtualapp/provider.py:17 +#: terminal/models/virtualapp/virtualapp.py:36 +#: terminal/models/virtualapp/virtualapp.py:97 +#: terminal/serializers/virtualapp.py:32 +msgid "Virtual app" +msgstr "虚拟应用" + +#: settings/serializers/feature.py:129 +msgid "Enable virtual app" +msgstr "启用虚拟应用" + #: settings/serializers/msg.py:24 msgid "SMTP host" msgstr "SMTP 主机" @@ -5744,7 +5913,7 @@ msgstr "多个用户,使用 , 分割" msgid "[%s] %s" msgstr "[%s] %s" -#: settings/serializers/terminal.py:9 +#: settings/serializers/terminal.py:9 terminal/models/virtualapp/provider.py:11 msgid "Hostname" msgstr "主机名" @@ -5806,100 +5975,104 @@ msgstr "周期导入 LDAP 用户" msgid "Registration periodic import ldap user task" msgstr "注册周期导入 LDAP 用户 任务" -#: settings/utils/ldap.py:494 +#: settings/utils/ldap.py:492 msgid "ldap:// or ldaps:// protocol is used." msgstr "使用 ldap:// 或 ldaps:// 协议" -#: settings/utils/ldap.py:505 +#: settings/utils/ldap.py:503 msgid "Host or port is disconnected: {}" msgstr "主机或端口不可连接: {}" -#: settings/utils/ldap.py:507 +#: settings/utils/ldap.py:505 msgid "The port is not the port of the LDAP service: {}" msgstr "端口不是LDAP服务端口: {}" -#: settings/utils/ldap.py:509 +#: settings/utils/ldap.py:507 msgid "Please add certificate: {}" msgstr "请添加证书" -#: settings/utils/ldap.py:513 settings/utils/ldap.py:540 -#: settings/utils/ldap.py:570 settings/utils/ldap.py:598 +#: settings/utils/ldap.py:511 settings/utils/ldap.py:538 +#: settings/utils/ldap.py:568 settings/utils/ldap.py:596 msgid "Unknown error: {}" msgstr "未知错误: {}" -#: settings/utils/ldap.py:527 +#: settings/utils/ldap.py:525 msgid "Bind DN or Password incorrect" msgstr "绑定DN或密码错误" -#: settings/utils/ldap.py:534 +#: settings/utils/ldap.py:532 msgid "Please enter Bind DN: {}" msgstr "请输入绑定DN: {}" -#: settings/utils/ldap.py:536 +#: settings/utils/ldap.py:534 msgid "Please enter Password: {}" msgstr "请输入密码: {}" -#: settings/utils/ldap.py:538 +#: settings/utils/ldap.py:536 msgid "Please enter correct Bind DN and Password: {}" msgstr "请输入正确的绑定DN和密码: {}" -#: settings/utils/ldap.py:556 +#: settings/utils/ldap.py:554 msgid "Invalid User OU or User search filter: {}" msgstr "不合法的用户OU或用户过滤器: {}" -#: settings/utils/ldap.py:587 +#: settings/utils/ldap.py:585 msgid "LDAP User attr map not include: {}" msgstr "LDAP属性映射没有包含: {}" -#: settings/utils/ldap.py:594 +#: settings/utils/ldap.py:592 msgid "LDAP User attr map is not dict" msgstr "LDAP属性映射不合法" -#: settings/utils/ldap.py:613 +#: settings/utils/ldap.py:611 msgid "LDAP authentication is not enabled" msgstr "LDAP认证没有启用" -#: settings/utils/ldap.py:631 +#: settings/utils/ldap.py:629 msgid "Error (Invalid LDAP server): {}" msgstr "错误 (不合法的LDAP服务器地址): {}" -#: settings/utils/ldap.py:633 +#: settings/utils/ldap.py:631 msgid "Error (Invalid Bind DN): {}" msgstr "错误 (不合法的绑定DN): {}" -#: settings/utils/ldap.py:635 +#: settings/utils/ldap.py:633 msgid "Error (Invalid LDAP User attr map): {}" msgstr "错误 (不合法的LDAP属性映射): {}" -#: settings/utils/ldap.py:637 +#: settings/utils/ldap.py:635 msgid "Error (Invalid User OU or User search filter): {}" msgstr "错误 (不合法的用户OU或用户过滤器): {}" -#: settings/utils/ldap.py:639 +#: settings/utils/ldap.py:637 msgid "Error (Not enabled LDAP authentication): {}" msgstr "错误 (没有启用LDAP认证): {}" -#: settings/utils/ldap.py:641 +#: settings/utils/ldap.py:639 msgid "Error (Unknown): {}" msgstr "错误 (未知): {}" -#: settings/utils/ldap.py:644 +#: settings/utils/ldap.py:642 msgid "Succeed: Match {} s user" msgstr "成功匹配 {} 个用户" -#: settings/utils/ldap.py:677 +#: settings/utils/ldap.py:653 +msgid "Please test the connection first" +msgstr "请先测试连接" + +#: settings/utils/ldap.py:675 msgid "Authentication failed (configuration incorrect): {}" msgstr "认证失败 (配置错误): {}" -#: settings/utils/ldap.py:681 +#: settings/utils/ldap.py:679 msgid "Authentication failed (username or password incorrect): {}" msgstr "认证失败 (用户名或密码不正确): {}" -#: settings/utils/ldap.py:683 +#: settings/utils/ldap.py:681 msgid "Authentication failed (Unknown): {}" msgstr "认证失败: (未知): {}" -#: settings/utils/ldap.py:686 +#: settings/utils/ldap.py:684 msgid "Authentication success: {}" msgstr "认证成功: {}" @@ -5959,10 +6132,6 @@ msgstr "管理页面" msgid "User page" msgstr "用户页面" -#: templates/_header_bar.html:90 -msgid "API Key" -msgstr "API Key" - #: templates/_header_bar.html:91 msgid "Logout" msgstr "注销登录" @@ -6059,7 +6228,7 @@ msgid "Home page" msgstr "首页" #: templates/resource_download.html:18 templates/resource_download.html:33 -#: users/const.py:53 +#: users/const.py:60 msgid "Client" msgstr "客户端" @@ -6099,11 +6268,13 @@ msgstr "OpenSSH 是在 windows 远程应用发布服务器中用来连接远程 msgid "Offline video player" msgstr "离线录像播放器" -#: terminal/api/applet/applet.py:48 terminal/api/applet/applet.py:51 +#: terminal/api/applet/applet.py:50 terminal/api/applet/applet.py:53 +#: terminal/api/virtualapp/virtualapp.py:43 +#: terminal/api/virtualapp/virtualapp.py:46 msgid "Invalid zip file" msgstr "无效的 zip 文件" -#: terminal/api/applet/applet.py:65 +#: terminal/api/applet/applet.py:72 msgid "This is enterprise edition applet" msgstr "企业版远程应用,在社区版中不能使用" @@ -6225,7 +6396,7 @@ msgstr "严重" msgid "High" msgstr "较高" -#: terminal/const.py:47 terminal/const.py:83 +#: terminal/const.py:47 terminal/const.py:84 #: users/templates/users/reset_password.html:50 msgid "Normal" msgstr "正常" @@ -6234,46 +6405,50 @@ msgstr "正常" msgid "Offline" msgstr "离线" -#: terminal/const.py:79 +#: terminal/const.py:80 msgid "Mismatch" msgstr "未匹配" -#: terminal/const.py:84 +#: terminal/const.py:85 msgid "Tunnel" msgstr "隧道" -#: terminal/const.py:90 +#: terminal/const.py:91 msgid "Read only" msgstr "只读" -#: terminal/const.py:91 +#: terminal/const.py:92 msgid "Writable" msgstr "读写" -#: terminal/const.py:95 +#: terminal/const.py:96 msgid "Kill session" msgstr "终断会话" -#: terminal/const.py:96 +#: terminal/const.py:97 msgid "Lock session" msgstr "锁定会话" -#: terminal/const.py:97 +#: terminal/const.py:98 msgid "Unlock session" msgstr "解锁会话" -#: terminal/const.py:102 +#: terminal/const.py:103 msgid "Replay create failed" msgstr "录像创建失败" -#: terminal/const.py:103 +#: terminal/const.py:104 msgid "Replay upload failed" msgstr "录像上传失败" -#: terminal/const.py:104 +#: terminal/const.py:105 msgid "Replay convert failed" msgstr "录像转码失败" +#: terminal/const.py:106 +msgid "Replay unsupported" +msgstr "不支持录像" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" @@ -6291,6 +6466,7 @@ msgid "Enterprise" msgstr "企业版" #: terminal/models/applet/applet.py:36 +#: terminal/models/virtualapp/virtualapp.py:22 msgid "Author" msgstr "作者" @@ -6303,6 +6479,7 @@ msgid "Can concurrent" msgstr "可以并发" #: terminal/models/applet/applet.py:44 +#: terminal/models/virtualapp/virtualapp.py:29 msgid "Tags" msgstr "标签" @@ -6311,6 +6488,7 @@ msgid "Hosts" msgstr "主机" #: terminal/models/applet/applet.py:93 +#: terminal/models/virtualapp/virtualapp.py:66 msgid "Applet pkg not valid, Missing file {}" msgstr "Applet pkg 无效,缺少文件 {}" @@ -6326,12 +6504,12 @@ msgstr "只支持自定义平台" msgid "Missing type in platform.yml" msgstr "在 platform.yml 中缺少类型" -#: terminal/models/applet/applet.py:317 terminal/models/applet/host.py:36 +#: terminal/models/applet/applet.py:318 terminal/models/applet/host.py:36 #: terminal/models/applet/host.py:138 msgid "Hosting" msgstr "宿主机" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:57 +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:69 msgid "Deploy options" msgstr "部署参数" @@ -6395,15 +6573,19 @@ msgstr "PostgreSQL 端口" msgid "Redis port" msgstr "Redis 端口" -#: terminal/models/component/endpoint.py:29 -#: terminal/models/component/endpoint.py:102 +#: terminal/models/component/endpoint.py:23 +msgid "SQLServer port" +msgstr "SQLServer 端口" + +#: terminal/models/component/endpoint.py:30 +#: terminal/models/component/endpoint.py:103 #: terminal/serializers/endpoint.py:73 terminal/serializers/storage.py:41 #: terminal/serializers/storage.py:53 terminal/serializers/storage.py:83 #: terminal/serializers/storage.py:93 terminal/serializers/storage.py:101 msgid "Endpoint" msgstr "端点" -#: terminal/models/component/endpoint.py:108 +#: terminal/models/component/endpoint.py:109 msgid "Endpoint rule" msgstr "端点规则" @@ -6461,7 +6643,7 @@ msgstr "远端地址" msgid "Application User" msgstr "应用用户" -#: terminal/models/component/terminal.py:166 +#: terminal/models/component/terminal.py:176 msgid "Can view terminal config" msgstr "可以查看终端配置" @@ -6583,6 +6765,19 @@ msgstr "验证码不正确" msgid "You have already joined this session" msgstr "您已经加入过此会话" +#: terminal/models/virtualapp/virtualapp.py:32 +msgid "Providers" +msgstr "提供商" + +#: terminal/models/virtualapp/virtualapp.py:94 +#: terminal/serializers/virtualapp.py:34 +msgid "App Provider" +msgstr "应用提供商" + +#: terminal/models/virtualapp/virtualapp.py:102 +msgid "Virtual app publication" +msgstr "虚拟应用发布" + #: terminal/notifications.py:25 msgid "Sessions" msgstr "会话管理" @@ -6620,7 +6815,7 @@ msgstr "测试失败: 账号无效" msgid "Invalid storage" msgstr "无效的存储" -#: terminal/serializers/applet.py:28 +#: terminal/serializers/applet.py:28 terminal/serializers/virtualapp.py:15 msgid "Icon" msgstr "图标" @@ -6673,19 +6868,36 @@ msgstr "RDS 授权模式" msgid "RDS Single Session Per User" msgstr "RDS 单用户单会话" -#: terminal/serializers/applet_host.py:52 -msgid "RDS Max Disconnection Time" -msgstr "RDS 最大断开时间" - #: terminal/serializers/applet_host.py:53 -msgid "RDS Remote App Logoff Time Limit" -msgstr "RDS 远程应用注销时间限制" +msgid "RDS Max Disconnection Time (ms)" +msgstr "RDS 最大断开时间(毫秒)" -#: terminal/serializers/applet_host.py:59 terminal/serializers/terminal.py:47 +#: terminal/serializers/applet_host.py:55 +msgid "" +"Tips: Set the maximum duration for keeping a disconnected session active on " +"the server (log off the session after 60000 milliseconds)." +msgstr "" +"提示:设置某个已断开连接的会话在服务器上能保持活动状态的最长时间(60000 毫秒" +"后注销会话)" + +#: terminal/serializers/applet_host.py:60 +msgid "RDS Remote App Logoff Time Limit (ms)" +msgstr "RDS 远程应用注销时间限制(毫秒)" + +#: terminal/serializers/applet_host.py:62 +msgid "" +"Tips: Set the logoff time for RemoteApp sessions after closing all RemoteApp " +"programs (0 milliseconds, log off the session immediately)." +msgstr "" +"提示:关闭所有 RemoteApp 程序之后设置 RemoteAPP 会话的注销时间(0 毫秒,立即" +"注销会话)" + +#: terminal/serializers/applet_host.py:71 terminal/serializers/terminal.py:47 +#: terminal/serializers/virtualapp_provider.py:13 msgid "Load status" msgstr "负载状态" -#: terminal/serializers/applet_host.py:73 +#: terminal/serializers/applet_host.py:85 msgid "" "These accounts are used to connect to the published application, the account " "is now divided into two types, one is dedicated to each account, each user " @@ -6696,13 +6908,13 @@ msgstr "" "这些账号用于连接发布的应用,账号现在分为两种类型:
一种是专用的,每个用" "户都有一个专用账号。 另一种是公共的,当应用不支持多开且专用的已经被使用时,会" "使用公共账号连接;
注意: 如果不开启自动创建账号, 当前发布机仅能被指定标" -"签的资产调度到,默认不会放到调度池中" +"签的资产调度到,默认不会放到调度池中,且需要手动维护账号" -#: terminal/serializers/applet_host.py:80 +#: terminal/serializers/applet_host.py:92 msgid "The number of public accounts created automatically" msgstr "公用账号自动创建的数量" -#: terminal/serializers/applet_host.py:83 +#: terminal/serializers/applet_host.py:95 msgid "" "Connect to the host using the same account first. For security reasons, " "please set the configuration item CACHE_LOGIN_PASSWORD_ENABLED=true and " @@ -6829,7 +7041,7 @@ msgstr "Access key ID(AK)" msgid "Access key secret" msgstr "Access key secret(SK)" -#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:250 +#: terminal/serializers/storage.py:68 xpack/plugins/cloud/models.py:253 msgid "Region" msgstr "地域" @@ -6849,8 +7061,8 @@ msgstr "端点后缀" msgid "HOST" msgstr "主机" -#: terminal/serializers/storage.py:146 users/models/user.py:823 -#: xpack/plugins/cloud/serializers/account_attrs.py:206 +#: terminal/serializers/storage.py:146 users/models/user.py:830 +#: xpack/plugins/cloud/serializers/account_attrs.py:213 msgid "Private key" msgstr "ssh私钥" @@ -6894,6 +7106,26 @@ msgstr "会话 ID" msgid "Not found" msgstr "没有发现" +#: terminal/serializers/virtualapp_provider.py:26 +msgid "Container ID" +msgstr "容器 ID" + +#: terminal/serializers/virtualapp_provider.py:27 +msgid "Container Image" +msgstr "容器镜像" + +#: terminal/serializers/virtualapp_provider.py:28 +msgid "Container Name" +msgstr "容器名称" + +#: terminal/serializers/virtualapp_provider.py:29 +msgid "Container Status" +msgstr "容器状态" + +#: terminal/serializers/virtualapp_provider.py:30 +msgid "Container Ports" +msgstr "容器端口" + #: terminal/tasks.py:33 msgid "Periodic delete terminal status" msgstr "周期清理终端状态" @@ -7171,19 +7403,19 @@ msgstr "工单基本信息" msgid "Ticket applied info" msgstr "工单申请信息" -#: tickets/notifications.py:109 +#: tickets/notifications.py:111 msgid "Your has a new ticket, applicant - {}" msgstr "你有一个新的工单, 申请人 - {}" -#: tickets/notifications.py:113 +#: tickets/notifications.py:115 msgid "{}: New Ticket - {} ({})" msgstr "新工单 - {} ({})" -#: tickets/notifications.py:157 +#: tickets/notifications.py:159 msgid "Your ticket has been processed, processor - {}" msgstr "你的工单已被处理, 处理人 - {}" -#: tickets/notifications.py:161 +#: tickets/notifications.py:163 msgid "Ticket has processed - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})" @@ -7236,11 +7468,11 @@ msgstr "授权名称 `{}` 已存在" msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" -#: tickets/templates/tickets/_msg_ticket.html:20 +#: tickets/templates/tickets/_msg_ticket.html:21 msgid "View details" msgstr "查看详情" -#: tickets/templates/tickets/_msg_ticket.html:25 +#: tickets/templates/tickets/_msg_ticket.html:26 msgid "Direct approval" msgstr "直接批准" @@ -7248,12 +7480,12 @@ msgstr "直接批准" msgid "Ticket information" msgstr "工单信息" -#: tickets/templates/tickets/approve_check_password.html:29 +#: tickets/templates/tickets/approve_check_password.html:28 #: tickets/views/approve.py:40 tickets/views/approve.py:77 msgid "Ticket approval" msgstr "工单审批" -#: tickets/templates/tickets/approve_check_password.html:44 +#: tickets/templates/tickets/approve_check_password.html:43 msgid "Approval" msgstr "同意" @@ -7278,11 +7510,11 @@ msgstr "无效的审批动作" msgid "This user is not authorized to approve this ticket" msgstr "此用户无权审批此工单" -#: users/api/user.py:141 +#: users/api/user.py:137 msgid "Can not invite self" msgstr "不能邀请自己" -#: users/api/user.py:194 +#: users/api/user.py:190 msgid "Could not reset self otp, use profile reset instead" msgstr "不能在该页面重置 MFA 多因子认证, 请去个人信息页面重置" @@ -7326,11 +7558,11 @@ msgstr "多屏显示" msgid "Drives redirect" msgstr "磁盘挂载" -#: users/const.py:57 +#: users/const.py:64 msgid "Replace" msgstr "替换" -#: users/const.py:58 +#: users/const.py:65 msgid "Suffix" msgstr "加后缀" @@ -7417,8 +7649,8 @@ msgstr "不能和原来的密钥相同" msgid "Not a valid ssh public key" msgstr "SSH密钥不合法" -#: users/forms/profile.py:173 users/models/user.py:826 -#: xpack/plugins/cloud/serializers/account_attrs.py:203 +#: users/forms/profile.py:173 users/models/user.py:833 +#: xpack/plugins/cloud/serializers/account_attrs.py:210 msgid "Public key" msgstr "SSH公钥" @@ -7426,72 +7658,74 @@ msgstr "SSH公钥" msgid "Preference" msgstr "用户设置" -#: users/models/user.py:643 users/serializers/profile.py:94 +#: users/models/user.py:646 users/serializers/profile.py:94 msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:805 users/serializers/user.py:169 +#: users/models/user.py:812 users/serializers/user.py:169 msgid "Is service account" msgstr "服务账号" -#: users/models/user.py:807 +#: users/models/user.py:814 msgid "Avatar" msgstr "头像" -#: users/models/user.py:810 +#: users/models/user.py:817 msgid "Wechat" msgstr "微信" -#: users/models/user.py:813 users/serializers/user.py:106 +#: users/models/user.py:820 users/serializers/user.py:106 msgid "Phone" msgstr "手机" -#: users/models/user.py:819 +#: users/models/user.py:826 msgid "OTP secret key" msgstr "OTP 密钥" -#: users/models/user.py:831 users/serializers/profile.py:128 +# msgid "Private key" +# msgstr "ssh私钥" +#: users/models/user.py:838 users/serializers/profile.py:128 #: users/serializers/user.py:166 msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:841 +#: users/models/user.py:848 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:844 +#: users/models/user.py:851 msgid "Need update password" msgstr "需要更新密码" -#: users/models/user.py:846 +#: users/models/user.py:853 msgid "Date api key used" msgstr "Api key 最后使用日期" -#: users/models/user.py:969 +#: users/models/user.py:984 msgid "Can not delete admin user" msgstr "无法删除管理员用户" -#: users/models/user.py:995 +#: users/models/user.py:1011 msgid "Can invite user" msgstr "可以邀请用户" -#: users/models/user.py:996 +#: users/models/user.py:1012 msgid "Can remove user" msgstr "可以移除用户" -#: users/models/user.py:997 +#: users/models/user.py:1013 msgid "Can match user" msgstr "可以匹配用户" -#: users/models/user.py:1006 +#: users/models/user.py:1022 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:1009 +#: users/models/user.py:1025 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" -#: users/models/user.py:1034 +#: users/models/user.py:1050 msgid "User password history" msgstr "用户密码历史" @@ -7563,37 +7797,45 @@ msgid "RDP client option" msgstr "RDP 客户端选项" #: users/serializers/preference/luna.py:45 -msgid "Rdp smart size" -msgstr "RDP 智能大小" +msgid "RDP color quality" +msgstr "" -#: users/serializers/preference/luna.py:46 +#: users/serializers/preference/luna.py:49 +msgid "Rdp smart size" +msgstr "" + +# msgid "Rdp smart size" +# msgstr "RDP 智能大小" +#: users/serializers/preference/luna.py:50 msgid "" "Determines whether the client computer should scale the content on the " "remote computer to fit the window size of the client computer when the " "window is resized." -msgstr "确定调整窗口大小时客户端计算机是否应缩放远程计算机上的内容以适应客户端计算机的窗口大小" +msgstr "" +"确定调整窗口大小时客户端计算机是否应缩放远程计算机上的内容以适应客户端计算机" +"的窗口大小" -#: users/serializers/preference/luna.py:51 +#: users/serializers/preference/luna.py:55 msgid "Remote application connection method" msgstr "远程应用连接方式" -#: users/serializers/preference/luna.py:58 +#: users/serializers/preference/luna.py:62 msgid "Character terminal font size" msgstr "字符终端字体大小" -#: users/serializers/preference/luna.py:61 +#: users/serializers/preference/luna.py:65 msgid "Backspace as Ctrl+H" msgstr "字符终端Backspace As Ctrl+H" -#: users/serializers/preference/luna.py:64 +#: users/serializers/preference/luna.py:68 msgid "Right click quickly paste" msgstr "右键快速粘贴" -#: users/serializers/preference/luna.py:70 +#: users/serializers/preference/luna.py:74 msgid "Graphics" msgstr "图形化" -#: users/serializers/preference/luna.py:71 +#: users/serializers/preference/luna.py:75 msgid "Command line" msgstr "命令行" @@ -7653,15 +7895,15 @@ msgstr "头像路径" msgid "MFA level" msgstr "MFA 级别" -#: users/serializers/user.py:282 +#: users/serializers/user.py:287 msgid "Select users" msgstr "选择用户" -#: users/serializers/user.py:283 +#: users/serializers/user.py:288 msgid "For security, only list several users" msgstr "为了安全,仅列出几个用户" -#: users/serializers/user.py:316 +#: users/serializers/user.py:321 msgid "name not unique" msgstr "名称重复" @@ -7691,9 +7933,9 @@ msgstr "周期检测用户过期" #: users/tasks.py:84 msgid "Check unused users" -msgstr "校验用户已过期" +msgstr "检查未使用的用户" -#: users/tasks.py:114 +#: users/tasks.py:123 msgid "The user has not logged in recently and has been disabled." msgstr "该用户最近未登录,已被禁用。" @@ -7947,6 +8189,11 @@ msgstr "重置密码成功,返回到登录页面" msgid "XPACK" msgstr "XPack" +#: xpack/exceptions.py:7 +msgid "" +"The current task is not synchronized with unmatched policy assets, skipping" +msgstr "" + #: xpack/plugins/cloud/api.py:56 msgid "Test connection successful" msgstr "测试成功" @@ -8027,47 +8274,67 @@ msgstr "天翼私有云" msgid "OpenStack" msgstr "OpenStack" -#: xpack/plugins/cloud/const.py:28 +#: xpack/plugins/cloud/const.py:28 xpack/plugins/cloud/providers/zstack.py:21 +msgid "ZStack" +msgstr "ZStack" + +#: xpack/plugins/cloud/const.py:29 msgid "Fusion Compute" msgstr "融合计算" -#: xpack/plugins/cloud/const.py:33 +#: xpack/plugins/cloud/const.py:30 +msgid "SCP" +msgstr "深信服SCP" + +#: xpack/plugins/cloud/const.py:31 +msgid "Apsara Stack" +msgstr "阿里云专有云" + +#: xpack/plugins/cloud/const.py:36 msgid "Private IP" msgstr "私有IP" -#: xpack/plugins/cloud/const.py:34 +#: xpack/plugins/cloud/const.py:37 msgid "Public IP" msgstr "公网IP" -#: xpack/plugins/cloud/const.py:38 xpack/plugins/cloud/models.py:295 +#: xpack/plugins/cloud/const.py:41 xpack/plugins/cloud/models.py:303 msgid "Instance name" msgstr "实例名称" -#: xpack/plugins/cloud/const.py:39 +#: xpack/plugins/cloud/const.py:42 msgid "Instance name and Partial IP" msgstr "实例名称和部分IP" -#: xpack/plugins/cloud/const.py:44 +#: xpack/plugins/cloud/const.py:47 msgid "Succeed" msgstr "成功" -#: xpack/plugins/cloud/const.py:48 +#: xpack/plugins/cloud/const.py:51 msgid "Unsync" msgstr "未同步" -#: xpack/plugins/cloud/const.py:49 +#: xpack/plugins/cloud/const.py:52 msgid "New Sync" msgstr "新同步" -#: xpack/plugins/cloud/const.py:50 +#: xpack/plugins/cloud/const.py:53 msgid "Synced" msgstr "已同步" -#: xpack/plugins/cloud/const.py:51 +#: xpack/plugins/cloud/const.py:54 msgid "Released" msgstr "已释放" -#: xpack/plugins/cloud/manager.py:54 +#: xpack/plugins/cloud/const.py:58 +msgid "And" +msgstr "与" + +#: xpack/plugins/cloud/const.py:59 +msgid "Or" +msgstr "或" + +#: xpack/plugins/cloud/manager.py:56 msgid "Account unavailable" msgstr "账号无效" @@ -8091,7 +8358,7 @@ msgstr "云账号" msgid "Test cloud account" msgstr "测试云账号" -#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:151 +#: xpack/plugins/cloud/models.py:92 xpack/plugins/cloud/serializers/task.py:159 msgid "Regions" msgstr "地域" @@ -8100,122 +8367,134 @@ msgid "Hostname strategy" msgstr "主机名策略" #: xpack/plugins/cloud/models.py:100 -#: xpack/plugins/cloud/serializers/task.py:154 +#: xpack/plugins/cloud/serializers/task.py:162 msgid "IP network segment group" msgstr "IP网段组" #: xpack/plugins/cloud/models.py:103 -#: xpack/plugins/cloud/serializers/task.py:159 +#: xpack/plugins/cloud/serializers/task.py:167 msgid "Sync IP type" msgstr "同步IP类型" #: xpack/plugins/cloud/models.py:106 -#: xpack/plugins/cloud/serializers/task.py:177 +#: xpack/plugins/cloud/serializers/task.py:185 msgid "Always update" msgstr "总是更新" -#: xpack/plugins/cloud/models.py:112 +#: xpack/plugins/cloud/models.py:108 +msgid "Fully synchronous" +msgstr "完全同步" + +#: xpack/plugins/cloud/models.py:113 msgid "Date last sync" msgstr "最后同步日期" -#: xpack/plugins/cloud/models.py:115 xpack/plugins/cloud/models.py:313 -#: xpack/plugins/cloud/models.py:337 +#: xpack/plugins/cloud/models.py:116 xpack/plugins/cloud/models.py:321 +#: xpack/plugins/cloud/models.py:345 msgid "Strategy" msgstr "策略" -#: xpack/plugins/cloud/models.py:120 xpack/plugins/cloud/models.py:197 +#: xpack/plugins/cloud/models.py:121 xpack/plugins/cloud/models.py:200 msgid "Sync instance task" msgstr "同步实例任务" -#: xpack/plugins/cloud/models.py:208 xpack/plugins/cloud/models.py:260 +#: xpack/plugins/cloud/models.py:211 xpack/plugins/cloud/models.py:263 msgid "Date sync" msgstr "同步日期" -#: xpack/plugins/cloud/models.py:212 +#: xpack/plugins/cloud/models.py:215 msgid "Sync instance snapshot" msgstr "同步实例快照" -#: xpack/plugins/cloud/models.py:216 +#: xpack/plugins/cloud/models.py:219 msgid "Sync instance task execution" msgstr "同步实例任务执行" -#: xpack/plugins/cloud/models.py:240 +#: xpack/plugins/cloud/models.py:243 msgid "Sync task" msgstr "同步任务" -#: xpack/plugins/cloud/models.py:244 +#: xpack/plugins/cloud/models.py:247 msgid "Sync instance task history" msgstr "同步实例任务历史" -#: xpack/plugins/cloud/models.py:247 +#: xpack/plugins/cloud/models.py:250 msgid "Instance" msgstr "实例" -#: xpack/plugins/cloud/models.py:264 +#: xpack/plugins/cloud/models.py:267 msgid "Sync instance detail" msgstr "同步实例详情" -#: xpack/plugins/cloud/models.py:281 +#: xpack/plugins/cloud/models.py:279 xpack/plugins/cloud/serializers/task.py:72 +msgid "Rule relation" +msgstr "条件关系" + +#: xpack/plugins/cloud/models.py:288 msgid "Task strategy" msgstr "任务策略" -#: xpack/plugins/cloud/models.py:285 +#: xpack/plugins/cloud/models.py:292 msgid "Equal" msgstr "等于" -#: xpack/plugins/cloud/models.py:286 +#: xpack/plugins/cloud/models.py:293 msgid "Not Equal" msgstr "不等于" -#: xpack/plugins/cloud/models.py:287 +#: xpack/plugins/cloud/models.py:294 msgid "In" msgstr "在...中" -#: xpack/plugins/cloud/models.py:288 +#: xpack/plugins/cloud/models.py:295 msgid "Contains" msgstr "包含" -#: xpack/plugins/cloud/models.py:289 +#: xpack/plugins/cloud/models.py:296 +msgid "Exclude" +msgstr "排除" + +#: xpack/plugins/cloud/models.py:297 msgid "Startswith" msgstr "以...开头" -#: xpack/plugins/cloud/models.py:290 +#: xpack/plugins/cloud/models.py:298 msgid "Endswith" msgstr "以...结尾" -#: xpack/plugins/cloud/models.py:296 +#: xpack/plugins/cloud/models.py:304 msgid "Instance platform" msgstr "实例平台" -#: xpack/plugins/cloud/models.py:297 +#: xpack/plugins/cloud/models.py:305 msgid "Instance address" msgstr "实例地址" -#: xpack/plugins/cloud/models.py:304 +#: xpack/plugins/cloud/models.py:312 msgid "Rule attr" msgstr "规则属性" -#: xpack/plugins/cloud/models.py:308 +#: xpack/plugins/cloud/models.py:316 msgid "Rule match" msgstr "规则匹配" -#: xpack/plugins/cloud/models.py:310 +#: xpack/plugins/cloud/models.py:318 msgid "Rule value" msgstr "规则值" -#: xpack/plugins/cloud/models.py:317 xpack/plugins/cloud/serializers/task.py:70 +#: xpack/plugins/cloud/models.py:325 xpack/plugins/cloud/serializers/task.py:75 msgid "Strategy rule" msgstr "条件" -#: xpack/plugins/cloud/models.py:332 +#: xpack/plugins/cloud/models.py:340 msgid "Action attr" msgstr "动作属性" -#: xpack/plugins/cloud/models.py:334 +#: xpack/plugins/cloud/models.py:342 msgid "Action value" msgstr "动作值" -#: xpack/plugins/cloud/models.py:341 xpack/plugins/cloud/serializers/task.py:73 +#: xpack/plugins/cloud/models.py:349 xpack/plugins/cloud/serializers/task.py:78 msgid "Strategy action" msgstr "动作" @@ -8409,11 +8688,11 @@ msgstr "TR-Istanbul" msgid "CN East-Suqian" msgstr "华东-宿迁" -#: xpack/plugins/cloud/serializers/account.py:64 +#: xpack/plugins/cloud/serializers/account.py:68 msgid "Validity display" msgstr "有效性显示" -#: xpack/plugins/cloud/serializers/account.py:65 +#: xpack/plugins/cloud/serializers/account.py:69 msgid "Provider display" msgstr "服务商显示" @@ -8430,50 +8709,50 @@ msgid "Subscription ID" msgstr "订阅 ID" #: xpack/plugins/cloud/serializers/account_attrs.py:98 -#: xpack/plugins/cloud/serializers/account_attrs.py:103 -#: xpack/plugins/cloud/serializers/account_attrs.py:119 -#: xpack/plugins/cloud/serializers/account_attrs.py:149 -#: xpack/plugins/cloud/serializers/account_attrs.py:199 +#: xpack/plugins/cloud/serializers/account_attrs.py:102 +#: xpack/plugins/cloud/serializers/account_attrs.py:126 +#: xpack/plugins/cloud/serializers/account_attrs.py:156 +#: xpack/plugins/cloud/serializers/account_attrs.py:206 msgid "API Endpoint" msgstr "API 端点" -#: xpack/plugins/cloud/serializers/account_attrs.py:109 +#: xpack/plugins/cloud/serializers/account_attrs.py:108 msgid "Auth url" msgstr "认证地址" -#: xpack/plugins/cloud/serializers/account_attrs.py:110 +#: xpack/plugins/cloud/serializers/account_attrs.py:109 msgid "eg: http://openstack.example.com:5000/v3" msgstr "如: http://openstack.example.com:5000/v3" -#: xpack/plugins/cloud/serializers/account_attrs.py:113 +#: xpack/plugins/cloud/serializers/account_attrs.py:112 msgid "User domain" msgstr "用户域" -#: xpack/plugins/cloud/serializers/account_attrs.py:120 +#: xpack/plugins/cloud/serializers/account_attrs.py:127 msgid "Cert File" msgstr "证书文件" -#: xpack/plugins/cloud/serializers/account_attrs.py:121 +#: xpack/plugins/cloud/serializers/account_attrs.py:128 msgid "Key File" msgstr "密钥文件" -#: xpack/plugins/cloud/serializers/account_attrs.py:137 +#: xpack/plugins/cloud/serializers/account_attrs.py:144 msgid "Service account key" msgstr "服务账号密钥" -#: xpack/plugins/cloud/serializers/account_attrs.py:138 +#: xpack/plugins/cloud/serializers/account_attrs.py:145 msgid "The file is in JSON format" msgstr "JSON 格式的文件" -#: xpack/plugins/cloud/serializers/account_attrs.py:156 +#: xpack/plugins/cloud/serializers/account_attrs.py:163 msgid "IP address invalid `{}`, {}" msgstr "IP 地址无效: `{}`, {}" -#: xpack/plugins/cloud/serializers/account_attrs.py:172 +#: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Such as: 192.168.1.0/24, 10.0.0.0-10.0.0.255" msgstr "例: 192.168.1.0/24,10.0.0.0-10.0.0.255" -#: xpack/plugins/cloud/serializers/account_attrs.py:175 +#: xpack/plugins/cloud/serializers/account_attrs.py:182 msgid "" "The port is used to detect the validity of the IP address. When the " "synchronization task is executed, only the valid IP address will be " @@ -8482,27 +8761,27 @@ msgstr "" "端口用来检测 IP 地址的有效性,在同步任务执行时,只会同步有效的 IP 地址。
" "如果端口为 0,则表示所有 IP 地址均有效。" -#: xpack/plugins/cloud/serializers/account_attrs.py:183 +#: xpack/plugins/cloud/serializers/account_attrs.py:190 msgid "Hostname prefix" msgstr "主机名前缀" -#: xpack/plugins/cloud/serializers/account_attrs.py:186 +#: xpack/plugins/cloud/serializers/account_attrs.py:193 msgid "IP segment" msgstr "IP 网段" -#: xpack/plugins/cloud/serializers/account_attrs.py:190 +#: xpack/plugins/cloud/serializers/account_attrs.py:197 msgid "Test port" msgstr "测试端口" -#: xpack/plugins/cloud/serializers/account_attrs.py:193 +#: xpack/plugins/cloud/serializers/account_attrs.py:200 msgid "Test timeout" msgstr "测试超时时间" -#: xpack/plugins/cloud/serializers/account_attrs.py:209 +#: xpack/plugins/cloud/serializers/account_attrs.py:216 msgid "Project" msgstr "project" -#: xpack/plugins/cloud/serializers/task.py:143 +#: xpack/plugins/cloud/serializers/task.py:151 msgid "" "Only instances matching the IP range will be synced.
If the instance " "contains multiple IP addresses, the first IP address that matches will be " @@ -8514,11 +8793,11 @@ msgstr "" "到的 IP 地址将被用作创建的资产的 IP。
默认值 * 表示同步所有实例和随机匹配 " "IP 地址。
例如: 192.168.1.0/24,10.1.1.1-10.1.1.20。" -#: xpack/plugins/cloud/serializers/task.py:149 +#: xpack/plugins/cloud/serializers/task.py:157 msgid "History count" msgstr "执行次数" -#: xpack/plugins/cloud/serializers/task.py:150 +#: xpack/plugins/cloud/serializers/task.py:158 msgid "Instance count" msgstr "实例个数" @@ -8562,7 +8841,15 @@ msgstr "退出页面logo" msgid "Theme" msgstr "主题" -#: xpack/plugins/interface/models.py:44 xpack/plugins/interface/models.py:85 +#: xpack/plugins/interface/models.py:42 +msgid "Beian link" +msgstr "公安联网备案跳转链接" + +#: xpack/plugins/interface/models.py:43 +msgid "Beian text" +msgstr "公安联网备案号" + +#: xpack/plugins/interface/models.py:46 xpack/plugins/interface/models.py:87 msgid "Interface setting" msgstr "界面设置" @@ -8594,17 +8881,17 @@ msgstr "企业专业版" msgid "Ultimate edition" msgstr "企业旗舰版" -#~ msgid "Copy" -#~ msgstr "复制" +#~ msgid "Password can not contains `{{` or `}}`" +#~ msgstr "密码不能包含 `{{` 或 `}}` 字符" -#~ msgid "Paste" -#~ msgstr "粘贴" +#~ msgid "Password can not contains `{%` or `%}`" +#~ msgstr "密码不能包含 `{%` 或 `%}` 字符" -#~ msgid "Password can not contains `'` " -#~ msgstr "密码不能包含 `'` 字符" +#~ msgid "FeiShu query user failed" +#~ msgstr "飞书查询用户失败" -#~ msgid "Password can not contains `\"` " -#~ msgstr "密码不能包含 `\"` 字符" +#~ msgid "The FeiShu is already bound to another user" +#~ msgstr "该飞书已经绑定其他用户" -#~ msgid "Object Storage" -#~ msgstr "对象存储" +#~ msgid "Binding FeiShu successfully" +#~ msgstr "绑定 飞书 成功" diff --git a/apps/notifications/backends/__init__.py b/apps/notifications/backends/__init__.py index 2e95bd437..047123e57 100644 --- a/apps/notifications/backends/__init__.py +++ b/apps/notifications/backends/__init__.py @@ -12,6 +12,7 @@ class BACKEND(models.TextChoices): DINGTALK = 'dingtalk', _('DingTalk') SITE_MSG = 'site_msg', _('Site message') FEISHU = 'feishu', _('FeiShu') + SLACK = 'slack', _('Slack') # SMS = 'sms', _('SMS') @property diff --git a/apps/notifications/backends/slack.py b/apps/notifications/backends/slack.py new file mode 100644 index 000000000..95e0bde60 --- /dev/null +++ b/apps/notifications/backends/slack.py @@ -0,0 +1,21 @@ +from django.conf import settings + +from common.sdk.im.slack import Slack as Client +from .base import BackendBase + + +class Slack(BackendBase): + account_field = 'slack_id' + is_enable_field_in_settings = 'AUTH_SLACK' + + def __init__(self): + self.client = Client( + bot_token=settings.SLACK_BOT_TOKEN, + ) + + def send_msg(self, users, message, subject=None): + accounts, __, __ = self.get_accounts(users) + return self.client.send_text(accounts, message) + + +backend = Slack diff --git a/apps/notifications/notifications.py b/apps/notifications/notifications.py index 19a86ac94..5ceb629c3 100644 --- a/apps/notifications/notifications.py +++ b/apps/notifications/notifications.py @@ -202,6 +202,9 @@ class Message(metaclass=MessageType): def get_site_msg_msg(self) -> dict: return self.html_msg + def get_slack_msg(self) -> dict: + return self.markdown_msg + def get_sms_msg(self) -> dict: return self.text_msg_with_sign diff --git a/apps/ops/ansible/inventory.py b/apps/ops/ansible/inventory.py index 1426ffc01..0dfa0b575 100644 --- a/apps/ops/ansible/inventory.py +++ b/apps/ops/ansible/inventory.py @@ -13,7 +13,7 @@ class JMSInventory: def __init__( self, assets, account_policy='privileged_first', account_prefer='root,Administrator', host_callback=None, - exclude_localhost=False, task_type=None + exclude_localhost=False, task_type=None, protocol=None ): """ :param assets: @@ -27,6 +27,7 @@ class JMSInventory: self.exclude_hosts = {} self.exclude_localhost = exclude_localhost self.task_type = task_type + self.protocol = protocol @staticmethod def clean_assets(assets): @@ -71,8 +72,9 @@ class JMSInventory: } if not account.secret: return var + if account.secret_type == 'password': - var['ansible_password'] = account.secret + var['ansible_password'] = account.escape_jinja2_syntax(account.secret) elif account.secret_type == 'ssh_key': var['ansible_ssh_private_key_file'] = account.private_key_path return var @@ -84,7 +86,7 @@ class JMSInventory: 'custom_become': True, 'custom_become_method': su_method, 'custom_become_user': account.su_from.username, - 'custom_become_password': account.su_from.secret, + 'custom_become_password': account.escape_jinja2_syntax(account.su_from.secret), 'custom_become_private_key_path': account.su_from.private_key_path } return var @@ -109,13 +111,13 @@ class JMSInventory: host.update(self.make_account_ansible_vars(account)) host['ansible_become'] = True host['ansible_become_user'] = 'root' - host['ansible_become_password'] = account.secret + host['ansible_become_password'] = account.escape_jinja2_syntax(account.secret) else: host.update(self.make_account_ansible_vars(account)) if gateway: ansible_connection = host.get('ansible_connection', 'ssh') - if ansible_connection in ('local', 'winrm'): + if ansible_connection in ('local', 'winrm', 'rdp'): host['gateway'] = { 'address': gateway.address, 'port': gateway.port, 'username': gateway.username, 'secret': gateway.password, @@ -127,19 +129,20 @@ class JMSInventory: host['jms_asset'].update(ansible_ssh_common_args) host.update(ansible_ssh_common_args) - @staticmethod - def get_primary_protocol(ansible_config, protocols): + def get_primary_protocol(self, ansible_config, protocols): invalid_protocol = type('protocol', (), {'name': 'null', 'port': 0}) ansible_connection = ansible_config.get('ansible_connection') # 数值越小,优先级越高,若用户在 ansible_config 中配置了,则提高用户配置方式的优先级 protocol_priority = {'ssh': 10, 'winrm': 9, ansible_connection: 1} + if self.protocol: + protocol_priority.update({self.protocol: 0}) protocol_sorted = sorted(protocols, key=lambda x: protocol_priority.get(x.name, 999)) protocol = protocol_sorted[0] if protocol_sorted else invalid_protocol return protocol @staticmethod def fill_ansible_config(ansible_config, protocol): - if protocol.name in ('ssh', 'winrm'): + if protocol.name in ('ssh', 'winrm', 'rdp'): ansible_config['ansible_connection'] = protocol.name if protocol.name == 'winrm': if protocol.setting.get('use_ssl', False): @@ -173,11 +176,13 @@ class JMSInventory: }, 'jms_account': { 'id': str(account.id), 'username': account.username, - 'secret': account.secret, 'secret_type': account.secret_type, - 'private_key_path': account.private_key_path + 'secret': account.escape_jinja2_syntax(account.secret), + 'secret_type': account.secret_type, 'private_key_path': account.private_key_path } if account else None } + protocols = host['jms_asset']['protocols'] + host['jms_asset'].update({f"{p['name']}_port": p['port'] for p in protocols}) if host['jms_account'] and tp == 'oracle': host['jms_account']['mode'] = 'sysdba' if account.privileged else None diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 4610ddfd2..c5fee5fd9 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -1,8 +1,9 @@ import os import uuid - +import shutil import ansible_runner from django.conf import settings +from django.utils._os import safe_join from .callback import DefaultCallback from ..utils import get_ansible_log_verbosity @@ -85,6 +86,34 @@ class PlaybookRunner: return self.cb +class UploadFileRunner: + def __init__(self, inventory, job_id, dest_path, callback=None): + self.id = uuid.uuid4() + self.inventory = inventory + self.cb = DefaultCallback() + upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file') + self.src_paths = safe_join(upload_file_dir, str(job_id)) + self.dest_path = safe_join("/tmp", dest_path) + + def run(self, verbosity=0, **kwargs): + verbosity = get_ansible_log_verbosity(verbosity) + ansible_runner.run( + host_pattern="*", + inventory=self.inventory, + module='copy', + module_args=f"src={self.src_paths}/ dest={self.dest_path}", + verbosity=verbosity, + event_handler=self.cb.event_handler, + status_handler=self.cb.status_handler, + **kwargs + ) + try: + shutil.rmtree(self.src_paths) + except OSError as e: + print(f"del upload tmp dir {self.src_paths} failed! {e}") + return self.cb + + class CommandRunner(AdHocRunner): def __init__(self, inventory, command, pattern='*', project_dir='/tmp/'): super().__init__(inventory, 'shell', command, pattern, project_dir) diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index a7baaf6e4..797d00c5e 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -7,14 +7,15 @@ from celery.result import AsyncResult from django.shortcuts import get_object_or_404 from django.utils.translation import gettext as _ from django_celery_beat.models import PeriodicTask +from django_filters import rest_framework as drf_filters from rest_framework import generics, viewsets, mixins, status from rest_framework.response import Response -from django_filters import rest_framework as drf_filters from common.api import LogTailApi, CommonApiMixin +from common.drf.filters import BaseFilterSet from common.exceptions import JMSException from common.permissions import IsValidUser -from common.drf.filters import BaseFilterSet +from common.utils.timezone import local_now from ops.celery import app from ..ansible.utils import get_ansible_task_log_path from ..celery.utils import get_celery_task_log_path @@ -138,6 +139,46 @@ class CeleryTaskViewSet( def get_queryset(self): return CeleryTask.objects.exclude(name__startswith='celery') + @staticmethod + def extract_schedule(input_string): + pattern = r'(\S+ \S+ \S+ \S+ \S+).*' + match = re.match(pattern, input_string) + if match: + return match.group(1) + else: + return input_string + + def generate_execute_time(self, queryset): + names = [i.name for i in queryset] + periodic_tasks = PeriodicTask.objects.filter(name__in=names) + periodic_task_dict = {task.name: task for task in periodic_tasks} + now = local_now() + for i in queryset: + task = periodic_task_dict.get(i.name) + if not task: + continue + i.exec_cycle = self.extract_schedule(str(task.scheduler)) + + last_run_at = task.last_run_at or now + next_run_at = task.schedule.remaining_estimate(last_run_at) + if next_run_at.total_seconds() < 0: + next_run_at = task.schedule.remaining_estimate(now) + i.next_exec_time = now + next_run_at + 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) + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + queryset = self.generate_execute_time(queryset) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + class CeleryTaskExecutionViewSet(CommonApiMixin, viewsets.ModelViewSet): serializer_class = CeleryTaskExecutionSerializer diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index f2bf92cae..f16fefde2 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -1,16 +1,22 @@ +import json +import os + from django.conf import settings from django.db import transaction from django.db.models import Count -from django.db.transaction import atomic from django.shortcuts import get_object_or_404 +from django.utils._os import safe_join +from django.utils.translation import gettext_lazy as _ +from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.views import APIView from assets.models import Asset +from common.const.http import POST from common.permissions import IsValidUser from ops.const import Types from ops.models import Job, JobExecution -from ops.serializers.job import JobSerializer, JobExecutionSerializer +from ops.serializers.job import JobSerializer, JobExecutionSerializer, FileSerializer __all__ = [ 'JobViewSet', 'JobExecutionViewSet', 'JobRunVariableHelpAPIView', @@ -24,6 +30,7 @@ from orgs.utils import tmp_to_org, get_current_org from accounts.models import Account from perms.models import PermNode from perms.utils import UserPermAssetUtil +from jumpserver.settings import get_file_md5 def set_task_to_serializer_data(serializer, task_id): @@ -61,7 +68,7 @@ class JobViewSet(OrgBulkModelViewSet): def get_queryset(self): queryset = super().get_queryset() - queryset = queryset.filter(creator=self.request.user) + queryset = queryset.filter(creator=self.request.user).exclude(type=Types.upload_file) if self.action != 'retrieve': return queryset.filter(instant=False) return queryset @@ -91,6 +98,65 @@ class JobViewSet(OrgBulkModelViewSet): transaction.on_commit( lambda: run_ops_job_execution.apply_async((str(execution.id),), task_id=str(execution.id))) + @staticmethod + def get_duplicates_files(files): + seen = set() + duplicates = set() + for file in files: + if file in seen: + duplicates.add(file) + else: + seen.add(file) + return list(duplicates) + + @staticmethod + def get_exceeds_limit_files(files): + exceeds_limit_files = [] + for file in files: + if file.size > settings.FILE_UPLOAD_SIZE_LIMIT_MB * 1024 * 1024: + exceeds_limit_files.append(file) + return exceeds_limit_files + + @action(methods=[POST], detail=False, serializer_class=FileSerializer, permission_classes=[IsValidUser, ], + url_path='upload') + def upload(self, request, *args, **kwargs): + uploaded_files = request.FILES.getlist('files') + serializer = self.get_serializer(data=request.data) + + if not serializer.is_valid(): + msg = 'Upload data invalid: {}'.format(serializer.errors) + return Response({'error': msg}, status=400) + + same_files = self.get_duplicates_files(uploaded_files) + if same_files: + return Response({'error': _("Duplicate file exists")}, status=400) + + exceeds_limit_files = self.get_exceeds_limit_files(uploaded_files) + if exceeds_limit_files: + return Response( + {'error': _("File size exceeds maximum limit. Please select a file smaller than {limit}MB").format( + limit=settings.FILE_UPLOAD_SIZE_LIMIT_MB)}, + status=400) + + job_id = request.data.get('job_id', '') + job = get_object_or_404(Job, pk=job_id) + job_args = json.loads(job.args) + src_path_info = [] + upload_file_dir = safe_join(settings.DATA_DIR, 'job_upload_file', job_id) + for uploaded_file in uploaded_files: + filename = uploaded_file.name + saved_path = safe_join(upload_file_dir, f'{filename}') + os.makedirs(os.path.dirname(saved_path), exist_ok=True) + with open(saved_path, 'wb+') as destination: + for chunk in uploaded_file.chunks(): + destination.write(chunk) + src_path_info.append({'filename': filename, 'md5': get_file_md5(saved_path)}) + job_args['src_path_info'] = src_path_info + job.args = json.dumps(job_args) + job.save() + self.run_job(job, serializer) + return Response({'task_id': serializer.data.get('task_id')}, status=201) + class JobExecutionViewSet(OrgBulkModelViewSet): serializer_class = JobExecutionSerializer @@ -101,7 +167,7 @@ class JobExecutionViewSet(OrgBulkModelViewSet): @staticmethod def start_deploy(instance, serializer): - task = run_ops_job_execution.apply_async((str(instance.id),), task_id=str(instance.id)) + run_ops_job_execution.apply_async((str(instance.id),), task_id=str(instance.id)) def perform_create(self, serializer): instance = serializer.save() @@ -113,7 +179,8 @@ class JobExecutionViewSet(OrgBulkModelViewSet): set_task_to_serializer_data(serializer, instance.id) transaction.on_commit( - lambda: run_ops_job_execution.apply_async((str(instance.id),), task_id=str(instance.id))) + lambda: run_ops_job_execution.apply_async((str(instance.id),), task_id=str(instance.id)) + ) def get_queryset(self): queryset = super().get_queryset() diff --git a/apps/ops/const.py b/apps/ops/const.py index 7fa636a0f..578697c48 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -37,6 +37,7 @@ class CreateMethods(models.TextChoices): class Types(models.TextChoices): adhoc = 'adhoc', _('Adhoc') playbook = 'playbook', _('Playbook') + upload_file = 'upload_file', _('Upload File') class RunasPolicies(models.TextChoices): diff --git a/apps/ops/migrations/0023_auto_20220912_0021.py b/apps/ops/migrations/0023_auto_20220912_0021.py index 0fc0de0ba..a55ba48f1 100644 --- a/apps/ops/migrations/0023_auto_20220912_0021.py +++ b/apps/ops/migrations/0023_auto_20220912_0021.py @@ -1,12 +1,13 @@ # Generated by Django 3.2.14 on 2022-12-28 10:03 +import uuid + +import django.db.models.deletion import private_storage.fields import private_storage.storage.files +import simple_history.models from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import simple_history.models -import uuid class Migration(migrations.Migration): @@ -70,8 +71,9 @@ class Migration(migrations.Migration): default='shell', max_length=128, null=True, verbose_name='Module')), ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', - max_length=128, verbose_name='Type')), + ('type', models.CharField( + choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook'), ('upload_file', 'Upload File')], + default='adhoc', max_length=128, verbose_name='Type')), ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), ('runas_policy', models.CharField( choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), @@ -173,8 +175,9 @@ class Migration(migrations.Migration): default='shell', max_length=128, null=True, verbose_name='Module')), ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', - max_length=128, verbose_name='Type')), + ('type', models.CharField( + choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook'), ('upload_file', 'Upload File')], + default='adhoc', max_length=128, verbose_name='Type')), ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), ('runas_policy', models.CharField( choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), diff --git a/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py index 26c5d2cd7..3b7192f60 100644 --- a/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py +++ b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py @@ -56,8 +56,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name='jobexecution', name='job_type', - field=models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', - max_length=128, verbose_name='Material Type'), + field=models.CharField( + choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook'), ('upload_file', 'Upload File')], + default='adhoc', max_length=128, verbose_name='Material Type'), ), migrations.AddField( model_name='jobexecution', diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 56d826e86..9fcc079ca 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -20,8 +20,9 @@ from simple_history.models import HistoricalRecords from accounts.models import Account from acls.models import CommandFilterACL from assets.models import Asset +from assets.automations.base.manager import SSHTunnelManager from common.db.encoder import ModelJSONFieldEncoder -from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner, CommandInBlackListException +from ops.ansible import JMSInventory, AdHocRunner, PlaybookRunner, CommandInBlackListException, UploadFileRunner from ops.mixin import PeriodTaskModelMixin from ops.variables import * from ops.const import Types, RunasPolicies, JobStatus, JobModules @@ -79,6 +80,13 @@ class JMSPermedInventory(JMSInventory): host['login_password'] = account.secret host['login_db'] = asset.spec_info.get('db_name', '') host['ansible_python_interpreter'] = sys.executable + if gateway: + host['gateway'] = { + 'address': gateway.address, 'port': gateway.port, + 'username': gateway.username, 'secret': gateway.password, + 'private_key_path': gateway.private_key_path + } + host['jms_asset']['port'] = protocol.port return host return super().make_account_vars(host, asset, account, automation, protocol, platform, gateway) @@ -126,17 +134,13 @@ class JMSPermedInventory(JMSInventory): class Job(JMSOrgBaseModel, PeriodTaskModelMixin): name = models.CharField(max_length=128, null=True, verbose_name=_('Name')) - instant = models.BooleanField(default=False) args = models.CharField(max_length=8192, default='', verbose_name=_('Args'), null=True, blank=True) module = models.CharField(max_length=128, choices=JobModules.choices, default=JobModules.shell, - verbose_name=_('Module'), - null=True) + verbose_name=_('Module'), null=True) chdir = models.CharField(default="", max_length=1024, verbose_name=_('Chdir'), null=True, blank=True) timeout = models.IntegerField(default=-1, verbose_name=_('Timeout (Seconds)')) - playbook = models.ForeignKey('ops.Playbook', verbose_name=_("Playbook"), null=True, on_delete=models.SET_NULL) - type = models.CharField(max_length=128, choices=Types.choices, default=Types.adhoc, verbose_name=_("Type")) creator = models.ForeignKey('users.User', verbose_name=_("Creator"), on_delete=models.SET_NULL, null=True) assets = models.ManyToManyField('assets.Asset', verbose_name=_("Assets")) @@ -320,7 +324,6 @@ class JobExecution(JMSOrgBaseModel): "login_password={{login_password}} " \ "login_port={{login_port}} " \ "%s={{login_db}}" % login_db_token - print(login_args) shell = "{} {}=\"{}\" ".format(login_args, query_token, self.current_job.args) return module, shell @@ -354,7 +357,7 @@ class JobExecution(JMSOrgBaseModel): static_variables = self.gather_static_variables() extra_vars.update(static_variables) - if self.current_job.type == 'adhoc': + if self.current_job.type == Types.adhoc: module, args = self.compile_shell() runner = AdHocRunner( @@ -366,10 +369,15 @@ class JobExecution(JMSOrgBaseModel): project_dir=self.private_dir, extra_vars=extra_vars, ) - elif self.current_job.type == 'playbook': + elif self.current_job.type == Types.playbook: runner = PlaybookRunner( self.inventory_path, self.current_job.playbook.entry ) + elif self.current_job.type == Types.upload_file: + job_id = self.current_job.id + args = json.loads(self.current_job.args) + dst_path = args.get('dst_path', '/') + runner = UploadFileRunner(self.inventory_path, job_id, dst_path) else: raise Exception("unsupported job type") return runner @@ -530,6 +538,8 @@ class JobExecution(JMSOrgBaseModel): self.before_start() runner = self.get_runner() + ssh_tunnel = SSHTunnelManager() + ssh_tunnel.local_gateway_prepare(runner) try: cb = runner.run(**kwargs) self.set_result(cb) @@ -540,6 +550,8 @@ class JobExecution(JMSOrgBaseModel): except Exception as e: logging.error(e, exc_info=True) self.set_error(e) + finally: + ssh_tunnel.local_gateway_clean(runner) class Meta: verbose_name = _("Job Execution") diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 5166aa755..a4e9c7e8d 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -30,9 +30,15 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer): class CeleryTaskSerializer(serializers.ModelSerializer): + exec_cycle = serializers.CharField(read_only=True) + next_exec_time = serializers.DateTimeField(format="%Y/%m/%d %H:%M:%S", read_only=True) + class Meta: model = CeleryTask - read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'date_last_publish'] + read_only_fields = [ + 'id', 'name', 'meta', 'summary', 'state', + 'date_last_publish', 'exec_cycle', 'next_exec_time' + ] fields = read_only_fields diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 011bea4e2..75729f988 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -3,9 +3,7 @@ import uuid from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.models import Node, Asset -from perms.models import PermNode -from perms.utils.user_perm import UserPermAssetUtil +from assets.models import Asset from common.serializers.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution @@ -18,14 +16,16 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): nodes = serializers.ListField(required=False, child=serializers.CharField()) date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) name = serializers.CharField(label=_('Name'), max_length=128, allow_blank=True, required=False) - assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects, many=True, - required=False) + assets = serializers.PrimaryKeyRelatedField(label=_('Assets'), queryset=Asset.objects, many=True, required=False) def to_internal_value(self, data): instant = data.get('instant', False) + job_type = data.get('type', '') + _uid = str(uuid.uuid4()).split('-')[-1] if instant: - _uid = str(uuid.uuid4()).split('-')[-1] data['name'] = f'job-{_uid}' + if job_type == 'upload_file': + data['name'] = f'upload_file-{_uid}' return super().to_internal_value(data) def get_request_user(self): @@ -36,7 +36,8 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): class Meta: model = Job read_only_fields = [ - "id", "date_last_run", "date_created", "date_updated", "average_time_cost" + "id", "date_last_run", "date_created", + "date_updated", "average_time_cost" ] fields = read_only_fields + [ "name", "instant", "type", "module", @@ -45,10 +46,17 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): "use_parameter_define", "parameters_define", "timeout", "chdir", "comment", "summary", "is_periodic", "interval", "crontab", "nodes", - "run_after_save", + "run_after_save" ] +class FileSerializer(serializers.Serializer): + files = serializers.FileField(allow_empty_file=False, max_length=128) + + class Meta: + ref_name = "JobFileSerializer" + + class JobExecutionSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) job_type = serializers.ReadOnlyField(label=_("Job type")) diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index eac89850b..1ff48906a 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -5,7 +5,6 @@ from rest_framework import serializers from common.serializers.fields import ReadableHiddenField from ops.models import Playbook from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from django.utils.translation import gettext_lazy as _ def parse_playbook_name(path): @@ -27,5 +26,6 @@ class PlaybookSerializer(BulkOrgResourceModelSerializer): model = Playbook read_only_fields = ["id", "date_created", "date_updated"] fields = read_only_fields + [ - "id", 'path', "name", "comment", "creator", 'create_method', 'vcs_url', + "id", 'path', "name", "comment", "creator", + 'create_method', 'vcs_url', ] diff --git a/apps/orgs/signal_handlers/common.py b/apps/orgs/signal_handlers/common.py index 83edeb425..57333a2e8 100644 --- a/apps/orgs/signal_handlers/common.py +++ b/apps/orgs/signal_handlers/common.py @@ -53,7 +53,7 @@ def subscribe_orgs_mapping_expire(sender, **kwargs): @delay_run(ttl=5) -def expire_user_orgs(*args): +def expire_user_orgs(): User.expire_users_rbac_perms_cache() diff --git a/apps/perms/migrations/0002_auto_20171228_0025_squashed_0009_auto_20180903_1132.py b/apps/perms/migrations/0002_auto_20171228_0025_squashed_0009_auto_20180903_1132.py index f7f191f4a..12d8e70ce 100644 --- a/apps/perms/migrations/0002_auto_20171228_0025_squashed_0009_auto_20180903_1132.py +++ b/apps/perms/migrations/0002_auto_20171228_0025_squashed_0009_auto_20180903_1132.py @@ -80,7 +80,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='assetpermission', name='nodes', - field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Nodes'), + field=models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='assets.Node', verbose_name='Node'), ), # migrations.RunPython( # code=migrate_node_permissions, diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index cee80f562..5ecf824c0 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -10,6 +10,7 @@ from accounts.models import Account from assets.models import Asset from common.utils import date_expired_default from common.utils.timezone import local_now +from labels.mixins import LabeledMixin from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import OrgManager from perms.const import ActionChoices @@ -56,7 +57,7 @@ def default_protocols(): return ['all'] -class AssetPermission(JMSOrgBaseModel): +class AssetPermission(LabeledMixin, JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) users = models.ManyToManyField( 'users.User', related_name='%(class)ss', blank=True, verbose_name=_("User") @@ -68,7 +69,7 @@ class AssetPermission(JMSOrgBaseModel): 'assets.Asset', related_name='granted_by_permissions', blank=True, verbose_name=_("Asset") ) nodes = models.ManyToManyField( - 'assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Nodes") + 'assets.Node', related_name='granted_by_permissions', blank=True, verbose_name=_("Node") ) # 特殊的账号: @ALL, @INPUT @USER 默认包含,将来在全局设置中进行控制. accounts = models.JSONField(default=list, verbose_name=_("Account")) diff --git a/apps/perms/notifications.py b/apps/perms/notifications.py index 2a8aa0c2d..5c82c2589 100644 --- a/apps/perms/notifications.py +++ b/apps/perms/notifications.py @@ -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 + self.day_count = _('today') if day_count == 0 else 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 + self.day_count = _('today') if day_count == 0 else day_count + _('day') def get_items_with_url(self): items_with_url = [] diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 26afba67c..d09c33538 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -4,9 +4,11 @@ from django.db.models import Q from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from accounts.const import Source from accounts.models import AccountTemplate, Account from accounts.tasks import push_accounts_to_assets_task from assets.models import Asset, Node +from common.serializers import ResourceLabelsMixin from common.serializers.fields import BitChoicesField, ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import ActionChoices, AssetPermission @@ -26,7 +28,7 @@ class ActionChoicesField(BitChoicesField): return data -class AssetPermissionSerializer(BulkOrgResourceModelSerializer): +class AssetPermissionSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer): users = ObjectRelatedField(queryset=User.objects, many=True, required=False, label=_('User')) user_groups = ObjectRelatedField( queryset=UserGroup.objects, many=True, required=False, label=_('User group') @@ -50,7 +52,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): "is_valid", "comment", "from_ticket", ] fields_small = fields_mini + fields_generic - fields_m2m = ["users", "user_groups", "assets", "nodes"] + fields_m2m = ["users", "user_groups", "assets", "nodes", "labels"] fields = fields_mini + fields_m2m + fields_generic read_only_fields = ["created_by", "date_created", "from_ticket"] extra_kwargs = { @@ -84,6 +86,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): ] for asset in assets: asset_exist_accounts = Account.objects.none() + asset_exist_account_names = asset.accounts.values_list('name', flat=True) for template in self.template_accounts: asset_exist_accounts |= asset.accounts.filter( username=template.username, @@ -95,11 +98,13 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): 'username': template.username, 'secret_type': template.secret_type } - if condition in username_secret_type_dict: + if condition in username_secret_type_dict or \ + template.name in asset_exist_account_names: continue account_data = {key: getattr(template, key) for key in account_attribute} account_data['su_from'] = template.get_su_from_account(asset) - account_data['name'] = f"{account_data['name']}-{_('Account template')}" + account_data['source'] = Source.TEMPLATE + account_data['source_id'] = str(template.id) need_create_accounts.append(Account(**{'asset_id': asset.id, **account_data})) return Account.objects.bulk_create(need_create_accounts) @@ -130,7 +135,7 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): """Perform necessary eager loading of data.""" queryset = queryset.prefetch_related( "users", "user_groups", "assets", "nodes", - ) + ).prefetch_related('labels', 'labels__label') return queryset @staticmethod diff --git a/apps/perms/templates/perms/_msg_item_permissions_expire.html b/apps/perms/templates/perms/_msg_item_permissions_expire.html index 9a9dc8244..eba2e7e5b 100644 --- a/apps/perms/templates/perms/_msg_item_permissions_expire.html +++ b/apps/perms/templates/perms/_msg_item_permissions_expire.html @@ -5,7 +5,7 @@

{% blocktranslate %} - The following {{ item_type }} will expire in {{ count }} days + The following {{ item_type }} will expire in {{ count }} {% endblocktranslate %}

diff --git a/apps/perms/templates/perms/_msg_permed_items_expire.html b/apps/perms/templates/perms/_msg_permed_items_expire.html index f4229b7eb..eb2a3cc47 100644 --- a/apps/perms/templates/perms/_msg_permed_items_expire.html +++ b/apps/perms/templates/perms/_msg_permed_items_expire.html @@ -5,7 +5,7 @@

{% blocktranslate %} - The following {{ item_type }} will expire in {{ count }} days + The following {{ item_type }} will expire in {{ count }} {% endblocktranslate %}

@@ -15,7 +15,7 @@ {% endfor %} -
+
-

{% trans 'If you have any question, please contact the administrator' %} diff --git a/apps/rbac/api/__init__.py b/apps/rbac/api/__init__.py index 894655045..28436525a 100644 --- a/apps/rbac/api/__init__.py +++ b/apps/rbac/api/__init__.py @@ -1,4 +1,4 @@ +from .content_type import * from .permission import * from .role import * from .rolebinding import * - diff --git a/apps/rbac/api/content_type.py b/apps/rbac/api/content_type.py new file mode 100644 index 000000000..2afe021ca --- /dev/null +++ b/apps/rbac/api/content_type.py @@ -0,0 +1,11 @@ +from rest_framework import viewsets + +from .. import serializers +from ..models import ContentType + + +class ContentTypeViewSet(viewsets.ModelViewSet): + serializer_class = serializers.ContentTypeSerializer + filterset_fields = ("app_label", "model",) + search_fields = filterset_fields + queryset = ContentType.objects.all() diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py index 387d0bc9c..b9133c5c5 100644 --- a/apps/rbac/api/role.py +++ b/apps/rbac/api/role.py @@ -56,9 +56,22 @@ class RoleViewSet(JMSModelViewSet): return instance.permissions.set(clone.get_permissions()) + def filter_builtins(self, queryset): + keyword = self.request.query_params.get('search') + if not keyword: + return queryset + + builtins = list(self.get_queryset().filter(builtin=True)) + matched = [role.id for role in builtins if keyword in role.display_name] + if not matched: + return queryset + queryset = list(queryset.exclude(id__in=matched)) + return queryset + builtins + def filter_queryset(self, queryset): queryset = super().filter_queryset(queryset) queryset = queryset.order_by(*self.ordering) + queryset = self.filter_builtins(queryset) return queryset def set_users_amount(self, queryset): diff --git a/apps/rbac/const.py b/apps/rbac/const.py index 01c8bd915..71eb47fbc 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -28,6 +28,7 @@ exclude_permissions = ( ('authentication', 'superconnectiontoken', 'change,delete', 'superconnectiontoken'), ('authentication', 'temptoken', 'delete', 'temptoken'), ('users', 'userpasswordhistory', '*', '*'), + ('users', 'usersession', '*', '*'), ('assets', 'adminuser', '*', '*'), ('assets', 'assetgroup', '*', '*'), ('assets', 'cluster', '*', '*'), @@ -73,7 +74,7 @@ exclude_permissions = ( ('perms', 'rebuildusertreetask', '*', '*'), ('perms', 'permedasset', '*', 'permedasset'), ('perms', 'permedapplication', 'add,change,delete', 'permedapplication'), - ('rbac', 'contenttype', '*', '*'), + ('rbac', 'contenttype', 'add,change,delete', '*'), ('rbac', 'permission', 'add,delete,change', 'permission'), ('rbac', 'rolebinding', '*', '*'), ('rbac', 'systemrolebinding', 'change', 'systemrolebinding'), diff --git a/apps/rbac/migrations/0014_auto_20231208_1548.py b/apps/rbac/migrations/0014_auto_20231208_1548.py new file mode 100644 index 000000000..5fb8cefb7 --- /dev/null +++ b/apps/rbac/migrations/0014_auto_20231208_1548.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.10 on 2023-12-08 07:48 + +from django.db import migrations + + +def migrate_update_offline_usersession_permission_name(apps, *args): + perm_model = apps.get_model('auth', 'Permission') + perm_model.objects.filter(codename='offline_usersession').update(name='Offline user session') + + +class Migration(migrations.Migration): + dependencies = [ + ('rbac', '0013_alter_menupermission_options'), + ] + + operations = [ + migrations.RunPython(migrate_update_offline_usersession_permission_name) + ] diff --git a/apps/rbac/models/permission.py b/apps/rbac/models/permission.py index 17653f151..2b597c3c9 100644 --- a/apps/rbac/models/permission.py +++ b/apps/rbac/models/permission.py @@ -1,8 +1,10 @@ +from django.apps import apps from django.contrib.auth.models import ContentType as DjangoContentType from django.contrib.auth.models import Permission as DjangoPermission from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from common.utils import lazyproperty from .. import const Scope = const.Scope @@ -14,10 +16,59 @@ class ContentType(DjangoContentType): class Meta: proxy = True + _apps_map = {} + @property def app_model(self): return '%s.%s' % (self.app_label, self.model) + @classmethod + def apps_map(cls): + from ..tree import app_nodes_data + if cls._apps_map: + return cls._apps_map + mapper = {} + for d in app_nodes_data: + i = d['id'] + name = d.get('name') + + if not name: + config = apps.get_app_config(d['id']) + if config: + name = config.verbose_name + if name: + mapper[i] = name + cls._apps_map = mapper + return mapper + + @property + def app_display(self): + return self.apps_map().get(self.app_label) + + @lazyproperty + def fields(self): + model = self.model_class() + return model._meta.fields + + @lazyproperty + def field_names(self): + return [f.name for f in self.fields] + + @lazyproperty + def filter_field_names(self): + names = [] + if 'name' in self.field_names: + names.append('name') + if 'address' in self.field_names: + names.append('address') + return names + + def filter_queryset(self, queryset, keyword): + q = Q() + for name in self.filter_field_names: + q |= Q(**{name + '__icontains': keyword}) + return queryset.filter(q) + class Permission(DjangoPermission): """ 权限类 """ diff --git a/apps/rbac/serializers/__init__.py b/apps/rbac/serializers/__init__.py index 894655045..28436525a 100644 --- a/apps/rbac/serializers/__init__.py +++ b/apps/rbac/serializers/__init__.py @@ -1,4 +1,4 @@ +from .content_type import * from .permission import * from .role import * from .rolebinding import * - diff --git a/apps/rbac/serializers/content_type.py b/apps/rbac/serializers/content_type.py new file mode 100644 index 000000000..8e811230c --- /dev/null +++ b/apps/rbac/serializers/content_type.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from ..models import ContentType + +__all__ = ['ContentTypeSerializer'] + + +class ContentTypeSerializer(serializers.ModelSerializer): + app_display = serializers.CharField() + + class Meta: + model = ContentType + fields = ('id', 'app_label', 'app_display', 'model', 'name') diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index 7a7a989f7..8e9a56831 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -39,7 +39,9 @@ app_nodes_data = [ {'id': 'rbac', 'view': 'view_console'}, {'id': 'settings', 'view': 'view_setting'}, {'id': 'tickets', 'view': 'view_other'}, + {'id': 'labels', 'view': 'view_console'}, {'id': 'authentication', 'view': 'view_other'}, + {'id': 'ops', 'view': 'view_workbench'}, ] # 额外其他节点,可以在不同的层次,需要指定父节点,可以将一些 model 归类到这个节点下面 diff --git a/apps/rbac/urls/api_urls.py b/apps/rbac/urls/api_urls.py index 5dc080930..f68aab11d 100644 --- a/apps/rbac/urls/api_urls.py +++ b/apps/rbac/urls/api_urls.py @@ -6,7 +6,6 @@ from .. import api app_name = 'rbac' - router = BulkRouter() router.register(r'roles', api.RoleViewSet, 'role') router.register(r'role-bindings', api.RoleBindingViewSet, 'role-binding') @@ -18,6 +17,7 @@ router.register(r'org-roles', api.OrgRoleViewSet, 'org-role') router.register(r'org-role-bindings', api.OrgRoleBindingViewSet, 'org-role-binding') router.register(r'permissions', api.PermissionViewSet, 'permission') +router.register(r'content-types', api.ContentTypeViewSet, 'content-type') system_role_router = routers.NestedDefaultRouter(router, r'system-roles', lookup='system_role') system_role_router.register(r'permissions', api.SystemRolePermissionsViewSet, 'system-role-permission') diff --git a/apps/settings/api/__init__.py b/apps/settings/api/__init__.py index 510925590..686a52f3b 100644 --- a/apps/settings/api/__init__.py +++ b/apps/settings/api/__init__.py @@ -1,3 +1,4 @@ +from .chat import * from .dingtalk import * from .email import * from .feishu import * @@ -5,6 +6,7 @@ from .ldap import * from .public import * from .security import * from .settings import * +from .slack import * from .sms import * from .vault import * from .wecom import * diff --git a/apps/settings/api/chat.py b/apps/settings/api/chat.py new file mode 100644 index 000000000..68a80f013 --- /dev/null +++ b/apps/settings/api/chat.py @@ -0,0 +1,119 @@ +import httpx +import openai +from django.conf import settings +from django.utils.translation import gettext_lazy as _ +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response + +from common.api import JMSModelViewSet +from common.permissions import IsValidUser, OnlySuperUser +from .. import serializers +from ..models import ChatPrompt +from ..prompt import DefaultChatPrompt + + +class ChatAITestingAPI(GenericAPIView): + serializer_class = serializers.ChatAISettingSerializer + rbac_perms = { + 'POST': 'settings.change_chatai' + } + + def get_config(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + data = self.serializer_class().data + data.update(serializer.validated_data) + for k, v in data.items(): + if v: + continue + # 页面没有传递值, 从 settings 中获取 + data[k] = getattr(settings, k, None) + return data + + def post(self, request): + config = self.get_config(request) + chat_ai_enabled = config['CHAT_AI_ENABLED'] + if not chat_ai_enabled: + return Response( + status=status.HTTP_400_BAD_REQUEST, + data={'msg': _('Chat AI is not enabled')} + ) + + proxy = config['GPT_PROXY'] + model = config['GPT_MODEL'] + + kwargs = { + 'base_url': config['GPT_BASE_URL'] or None, + 'api_key': config['GPT_API_KEY'], + } + try: + if proxy: + kwargs['http_client'] = httpx.Client( + proxies=proxy, + transport=httpx.HTTPTransport(local_address='0.0.0.0') + ) + client = openai.OpenAI(**kwargs) + + ok = False + error = '' + + client.chat.completions.create( + messages=[ + { + "role": "user", + "content": "Say this is a test", + } + ], + model=model, + ) + ok = True + except openai.APIConnectionError as e: + error = str(e.__cause__) # an underlying Exception, likely raised within httpx. + except openai.APIStatusError as e: + error = str(e.message) + except Exception as e: + ok, error = False, str(e) + + if ok: + _status, msg = status.HTTP_200_OK, _('Test success') + else: + _status, msg = status.HTTP_400_BAD_REQUEST, error + + return Response(status=_status, data={'msg': msg}) + + +class ChatPromptViewSet(JMSModelViewSet): + serializer_classes = { + 'default': serializers.ChatPromptSerializer, + } + permission_classes = [IsValidUser] + queryset = ChatPrompt.objects.all() + http_method_names = ['get', 'options'] + filterset_fields = ['name'] + search_fields = filterset_fields + + def get_permissions(self): + if self.action in ['create', 'update', 'partial_update', 'destroy']: + self.permission_classes = [OnlySuperUser] + return super().get_permissions() + + def filter_default_prompts(self): + lang = self.request.LANGUAGE_CODE + default_prompts = DefaultChatPrompt.get_prompts(lang) + search_query = self.request.query_params.get('search') + search_query = search_query or self.request.query_params.get('name') + if not search_query: + return default_prompts + + search_query = search_query.lower() + filtered_prompts = [ + prompt for prompt in default_prompts + if search_query in prompt['name'].lower() + ] + return filtered_prompts + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + default_prompts = self.filter_default_prompts() + return list(queryset) + default_prompts diff --git a/apps/settings/api/ldap.py b/apps/settings/api/ldap.py index 486da3562..9c619e24d 100644 --- a/apps/settings/api/ldap.py +++ b/apps/settings/api/ldap.py @@ -28,71 +28,6 @@ from ..utils import ( logger = get_logger(__file__) -class LDAPTestingConfigAPI(AsyncApiMixin, CreateAPIView): - serializer_class = LDAPTestConfigSerializer - perm_model = Setting - rbac_perms = { - 'POST': 'settings.change_auth', - 'create': 'settings.change_auth', - } - - def is_need_async(self): - return True - - def create(self, request, *args, **kwargs): - serializer = self.serializer_class(data=request.data) - if not serializer.is_valid(): - return Response({"error": str(serializer.errors)}, status=400) - config = self.get_ldap_config(serializer) - ok, msg = LDAPTestUtil(config).test_config() - status = 200 if ok else 400 - return Response(msg, status=status) - - @staticmethod - def get_ldap_config(serializer): - server_uri = serializer.validated_data["AUTH_LDAP_SERVER_URI"] - bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] - password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] - use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) - search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] - search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] - attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] - auth_ldap = serializer.validated_data.get('AUTH_LDAP', False) - - if not password: - password = settings.AUTH_LDAP_BIND_PASSWORD - - config = { - 'server_uri': server_uri, - 'bind_dn': bind_dn, - 'password': password, - 'use_ssl': use_ssl, - 'search_ou': search_ou, - 'search_filter': search_filter, - 'attr_map': attr_map, - 'auth_ldap': auth_ldap - } - return config - - -class LDAPTestingLoginAPI(APIView): - serializer_class = LDAPTestLoginSerializer - perm_model = Setting - rbac_perms = { - 'POST': 'settings.change_auth' - } - - def post(self, request): - serializer = self.serializer_class(data=request.data) - if not serializer.is_valid(): - return Response({"error": str(serializer.errors)}, status=400) - username = serializer.validated_data['username'] - password = serializer.validated_data['password'] - ok, msg = LDAPTestUtil().test_login(username, password) - status = 200 if ok else 400 - return Response(msg, status=status) - - class LDAPUserListApi(generics.ListAPIView): serializer_class = LDAPUserSerializer perm_model = Setting @@ -162,31 +97,10 @@ class LDAPUserListApi(generics.ListAPIView): # 缓存有数据 if queryset is not None: return super().list(request, *args, **kwargs) - - sync_util = LDAPSyncUtil() - # 还没有同步任务 - if sync_util.task_no_start: - ok, msg = LDAPTestUtil().test_config() - if not ok: - return Response(data={'msg': msg}, status=400) - # 任务外部设置 task running 状态 - sync_util.set_task_status(sync_util.TASK_STATUS_IS_RUNNING) - t = threading.Thread(target=sync_ldap_user) - t.start() - data = {'msg': _('Synchronization start, please wait.')} - return Response(data=data, status=409) - # 同步任务正在执行 - if sync_util.task_is_running: - data = {'msg': _('Synchronization is running, please wait.')} - return Response(data=data, status=409) - # 同步任务执行结束 - if sync_util.task_is_over: - msg = sync_util.get_task_error_msg() - data = {'error': _('Synchronization error: {}'.format(msg))} + else: + data = {'msg': _('Users are not synchronized, please click the user synchronization button')} return Response(data=data, status=400) - return super().list(request, *args, **kwargs) - class LDAPUserImportAPI(APIView): perm_model = Setting @@ -232,18 +146,3 @@ class LDAPUserImportAPI(APIView): return Response({ 'msg': _('Imported {} users successfully (Organization: {})').format(count, orgs_name) }) - - -class LDAPCacheRefreshAPI(generics.RetrieveAPIView): - perm_model = Setting - rbac_perms = { - 'retrieve': 'settings.change_auth' - } - - def retrieve(self, request, *args, **kwargs): - try: - LDAPSyncUtil().clear_cache() - except Exception as e: - logger.error(str(e)) - return Response(data={'msg': str(e)}, status=400) - return Response(data={'msg': 'success'}) diff --git a/apps/settings/api/settings.py b/apps/settings/api/settings.py index 6b5f096cf..c03e99b6c 100644 --- a/apps/settings/api/settings.py +++ b/apps/settings/api/settings.py @@ -39,6 +39,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'wecom': serializers.WeComSettingSerializer, 'dingtalk': serializers.DingTalkSettingSerializer, 'feishu': serializers.FeiShuSettingSerializer, + 'slack': serializers.SlackSettingSerializer, 'auth': serializers.AuthSettingSerializer, 'oidc': serializers.OIDCSettingSerializer, 'keycloak': serializers.KeycloakSettingSerializer, @@ -56,9 +57,11 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'cmpp2': serializers.CMPP2SMSSettingSerializer, 'custom': serializers.CustomSMSSettingSerializer, 'vault': serializers.VaultSettingSerializer, + 'chat': serializers.ChatAISettingSerializer, 'announcement': serializers.AnnouncementSettingSerializer, 'ticket': serializers.TicketSettingSerializer, 'ops': serializers.OpsSettingSerializer, + 'virtualapp': serializers.VirtualAppSerializer, } rbac_category_permissions = { @@ -66,6 +69,7 @@ class SettingsApi(generics.RetrieveUpdateAPIView): 'terminal': 'settings.change_terminal', 'ops': 'settings.change_ops', 'ticket': 'settings.change_ticket', + 'virtualapp': 'settings.change_virtualapp', 'announcement': 'settings.change_announcement', 'security': 'settings.change_security', 'security_basic': 'settings.change_security', diff --git a/apps/settings/api/slack.py b/apps/settings/api/slack.py new file mode 100644 index 000000000..3a4ad289d --- /dev/null +++ b/apps/settings/api/slack.py @@ -0,0 +1,40 @@ +from rest_framework.views import Response +from rest_framework.generics import GenericAPIView +from rest_framework.exceptions import APIException +from rest_framework import status +from django.utils.translation import gettext_lazy as _ + +from settings.models import Setting +from common.sdk.im.slack import Slack + +from .. import serializers + + +class SlackTestingAPI(GenericAPIView): + serializer_class = serializers.SlackSettingSerializer + rbac_perms = { + 'POST': 'settings.change_auth' + } + + def post(self, request): + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + + bot_token = serializer.validated_data.get('SLACK_BOT_TOKEN') + if not bot_token: + secret = Setting.objects.filter(name='SLACK_BOT_TOKEN').first() + if secret: + bot_token = secret.cleaned_value + + bot_token = bot_token or '' + + try: + slack = Slack(bot_token=bot_token) + slack.is_valid() + return Response(status=status.HTTP_200_OK, data={'msg': _('Test success')}) + except APIException as e: + try: + error = e.detail['errmsg'] + except: + error = e.detail + return Response(status=status.HTTP_400_BAD_REQUEST, data={'error': error}) diff --git a/apps/settings/migrations/0010_alter_setting_options.py b/apps/settings/migrations/0010_alter_setting_options.py index 3d52a64da..0ce372e76 100644 --- a/apps/settings/migrations/0010_alter_setting_options.py +++ b/apps/settings/migrations/0010_alter_setting_options.py @@ -4,7 +4,6 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ ('settings', '0009_alter_cas_username_attribute'), ] @@ -12,6 +11,22 @@ class Migration(migrations.Migration): operations = [ migrations.AlterModelOptions( name='setting', - options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_ops', 'Can change auth ops'), ('change_ticket', 'Can change auth ticket'), ('change_announcement', 'Can change auth announcement'), ('change_vault', 'Can change vault setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'}, + options={'permissions': [ + ('change_email', 'Can change email setting'), + ('change_auth', 'Can change auth setting'), + ('change_ops', 'Can change auth ops'), + ('change_ticket', 'Can change auth ticket'), + ('change_announcement', 'Can change auth announcement'), + ('change_vault', 'Can change vault setting'), + ('change_chatai', 'Can change chat ai setting'), + ('change_systemmsgsubscription', 'Can change system msg sub setting'), + ('change_sms', 'Can change sms setting'), + ('change_security', 'Can change security setting'), + ('change_clean', 'Can change clean setting'), + ('change_interface', 'Can change interface setting'), + ('change_license', 'Can change license setting'), + ('change_terminal', 'Can change terminal setting'), + ('change_other', 'Can change other setting') + ], 'verbose_name': 'System setting'}, ), ] diff --git a/apps/settings/migrations/0011_chatprompt.py b/apps/settings/migrations/0011_chatprompt.py new file mode 100644 index 000000000..1e2b05801 --- /dev/null +++ b/apps/settings/migrations/0011_chatprompt.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.10 on 2023-12-13 11:07 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('settings', '0010_alter_setting_options'), + ] + + operations = [ + migrations.CreateModel( + name='ChatPrompt', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('content', models.TextField(verbose_name='Content')), + ('builtin', models.BooleanField(default=False, verbose_name='Builtin')), + ], + options={ + 'verbose_name': 'Chat prompt', + }, + ), + ] diff --git a/apps/settings/migrations/0012_alter_setting_options.py b/apps/settings/migrations/0012_alter_setting_options.py new file mode 100644 index 000000000..f35da135f --- /dev/null +++ b/apps/settings/migrations/0012_alter_setting_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.10 on 2023-12-20 07:51 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('settings', '0011_chatprompt'), + ] + + operations = [ + migrations.AlterModelOptions( + name='setting', + options={'permissions': [('change_email', 'Can change email setting'), ('change_auth', 'Can change auth setting'), ('change_ops', 'Can change auth ops'), ('change_ticket', 'Can change auth ticket'), ('change_virtualapp', 'Can change virtual app setting'), ('change_announcement', 'Can change auth announcement'), ('change_vault', 'Can change vault setting'), ('change_chatai', 'Can change chat ai setting'), ('change_systemmsgsubscription', 'Can change system msg sub setting'), ('change_sms', 'Can change sms setting'), ('change_security', 'Can change security setting'), ('change_clean', 'Can change clean setting'), ('change_interface', 'Can change interface setting'), ('change_license', 'Can change license setting'), ('change_terminal', 'Can change terminal setting'), ('change_other', 'Can change other setting')], 'verbose_name': 'System setting'}, + ), + ] diff --git a/apps/settings/models.py b/apps/settings/models.py index dc599bbe4..d6cda11df 100644 --- a/apps/settings/models.py +++ b/apps/settings/models.py @@ -8,6 +8,7 @@ from django.db import models from django.db.utils import ProgrammingError, OperationalError from django.utils.translation import gettext_lazy as _ +from common.db.models import JMSBaseModel from common.utils import signer, get_logger logger = get_logger(__name__) @@ -161,8 +162,10 @@ class Setting(models.Model): ('change_auth', _('Can change auth setting')), ('change_ops', _('Can change auth ops')), ('change_ticket', _('Can change auth ticket')), + ('change_virtualapp', _('Can change virtual app setting')), ('change_announcement', _('Can change auth announcement')), ('change_vault', _('Can change vault setting')), + ('change_chatai', _('Can change chat ai setting')), ('change_systemmsgsubscription', _('Can change system msg sub setting')), ('change_sms', _('Can change sms setting')), ('change_security', _('Can change security setting')), @@ -172,3 +175,15 @@ class Setting(models.Model): ('change_terminal', _('Can change terminal setting')), ('change_other', _('Can change other setting')), ] + + +class ChatPrompt(JMSBaseModel): + name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) + content = models.TextField(blank=False, null=False, verbose_name=_('Content')) + builtin = models.BooleanField(default=False, verbose_name=_('Builtin')) + + class Meta: + verbose_name = _("Chat prompt") + + def __str__(self): + return self.name diff --git a/apps/settings/prompt.py b/apps/settings/prompt.py new file mode 100644 index 000000000..5e2b72c96 --- /dev/null +++ b/apps/settings/prompt.py @@ -0,0 +1,197 @@ +class DefaultChatPrompt: + DEFAULT = { + 'zh': [ + { + 'name': '周报生成器', + 'content': '使用下面提供的文本作为中文周报的基础,生成一个简洁的摘要,突出最重要的内容。该报告应以 markdown 格式编写,' + '并应易于阅读和理解,以满足一般受众的需要。特别是要注重提供对利益相关者和决策者有用的见解和分析。' + '你也可以根据需要使用任何额外的信息或来源。', + }, + { + 'name': '数据库专家', + 'content': '我希望你充当一个数据库专家的角色,当我问你 sql 相关的问题时,' + '我需要你转换为标准的 sql 语句,当我的描述不够精准时,请给出合适的反馈', + }, + { + 'name': '全栈程序员', + 'content': '我希望你能扮演一个软件开发者的角色。我将提供一些关于网络应用需求的具体信息,' + '而你的工作是提出一个架构和代码,用 Golang 和 Angular 开发安全的应用。', + }, + { + 'name': '前端开发', + 'content': '我希望你能担任高级前端开发员。我将描述一个项目的细节,你将用这些工具来编码项目。' + 'Create React App, yarn, Ant Design, List, Redux Toolkit, createSlice, thunk, axios. ' + '你应该将文件合并到单一的 index.js 文件中,而不是其他。不要写解释。', + }, + { + 'name': '架构师 IT', + 'content': '我希望你能扮演一个 IT 架构师的角色。我将提供一些关于应用程序或其他数字产品功能的细节,' + '而你的工作是想出将其整合到 IT 环境中的方法。这可能涉及到分析业务需求,进行差距分析,' + '并将新系统的功能映射到现有的 IT 环境中。接下来的步骤是创建一个解决方案设计,' + '一个物理网络蓝图,定义系统集成的接口和部署环境的蓝图。', + }, + { + 'name': '代码释义器', + 'content': '我希望你能充当代码解释者,阐明代码的语法和语义。', + }, + { + 'name': 'IT 编程问题', + 'content': '我想让你充当 Stackoverflow 的帖子。我将提出与编程有关的问题,你将回答答案是什么。' + '我希望你只回答给定的答案,在没有足够的细节时写出解释。当我需要用英语告诉你一些事情时,我会把文字放在大括号里{像这样}。' + }, + { + 'name': '小红书风格', + 'content': '请使用 Emoji 风格编辑以下段落,该风格以引人入胜的标题、' + '每个段落中包含表情符号和在末尾添加相关标签为特点。请确保保持原文的意思。', + }, + { + 'name': '写作助理', + 'content': '作为一名中文写作改进助理,你的任务是改进所提供文本的拼写、语法、清晰、简洁和整体可读性,' + '同时分解长句,减少重复,并提供改进建议。请只提供文本的更正版本,避免包括解释。', + }, + { + 'name': 'Nature 风格润色', + 'content': '我希望你能充当专业的拼写和语法校对者,并改进我的文章。' + '我想让你用更美丽、优雅、高级的英语单词和句子替换我的简化 A0 级别的单词和句子,' + '保持意思不变,但使它们更具文学性,在《自然》杂志风格中提高我的表达水平。', + }, + ], + 'en': [ + { + "name": "Weekly Report Generator", + "content": "Using the text provided below as a basis for a Chinese weekly report, " + "generate a concise summary that highlights the most important content. " + "The report should be written in markdown format and should be easy to read " + "and understand for a general audience, especially focusing on providing insights " + "and analysis useful to stakeholders and decision-makers. You may also use any additional " + "information or sources as needed." + }, + { + "name": "Database Expert", + "content": "I want you to act as a database expert. When I ask you questions related to SQL, " + "I need you to convert them into standard SQL statements. " + "Please provide appropriate feedback when my descriptions are not precise enough." + }, + { + "name": "Full-Stack Programmer", + "content": "I want you to play the role of a software developer. " + "I will provide specific information about web application requirements, " + "and your job is to propose an architecture and code for developing a secure application " + "using Golang and Angular." + }, + { + "name": "Front-End Developer", + "content": "I want you to act as a senior front-end developer. " + "I will describe the details of a project, and you will code the project using these tools:" + " Create React App, yarn, Ant Design, List, Redux Toolkit, createSlice, thunk, axios. " + "You should consolidate files into a single index.js file, and not write explanations." + }, + { + "name": "IT Architect", + "content": "I want you to play the role of an IT architect. " + "I will provide details about the functionality of applications or other digital products, " + "and your job is to figure out how to integrate them into the IT environment. " + "This may involve analyzing business requirements, conducting gap analysis, " + "and mapping the new system's features to the existing IT environment. " + "The next steps are to create a solution design, a physical network blueprint, " + "define interfaces for system integration, and a blueprint for the deployment environment." + }, + { + "name": "Code Interpreter", + "content": "I want you to act as a code interpreter, clarifying the syntax and semantics of code." + }, + { + "name": "IT Programming Questions", + "content": "I want you to act as a Stackoverflow post. I will ask questions related to programming, " + "and you will answer what the answer is. Write out explanations " + "when there are not enough details. When I need to tell you something in English, " + "I will enclose the text in braces {like this}." + }, + { + "name": "Xiaohongshu Style", + "content": "Please edit the following paragraphs in Emoji style. " + "This style is characterized by engaging titles, the inclusion of emojis in each paragraph, " + "and adding related tags at the end. Ensure the original meaning is maintained." + }, + { + "name": "Writing Assistant", + "content": "As a Chinese writing improvement assistant, " + "your task is to improve the provided text in terms of spelling, grammar, clarity, " + "conciseness, and overall readability. Also, break down long sentences, reduce repetition, " + "and provide suggestions for improvement. Please only provide the corrected version of " + "the text, avoiding including explanations." + }, + { + "name": "Nature Style Editing", + "content": "I want you to act as a professional spelling and grammar proofreader and improve " + "my article. I want you to replace my simplified A0 level words and sentences with " + "more beautiful, elegant, and advanced English words and sentences. Keep the meaning " + "the same but make them more literary, enhancing my expression in the style of 'Nature' " + "magazine." + }, + ], + 'ja': [ + { + "name": "週報ジェネレータ", + "content": "以下のテキストを基にして中国語の週報の簡潔な要約を作成し、最も重要な内容を強調してください。" + "このレポートはマークダウン形式で書かれ、一般の聴衆にとって読みやすく理解しやすいものでなければなりません。" + "特に、利害関係者や意思決定者に有用な洞察と分析を提供することに重点を置いてください。" + "必要に応じて、追加の情報やソースを使用しても構いません。" + }, + { + "name": "データベースの専門家", + "content": "データベース専門家として機能し、私がsqlに関連する質問をするとき、" + "それを標準のsqlステートメントに変換してください。私の説明が不正確な場合は、適切なフィードバックを提供してください。" + }, + { + "name": "フルスタックプログラマー", + "content": "ソフトウェア開発者の役割を果たしてください。私はウェブアプリケーションの要件に関する具体的な情報を提供します。" + "あなたの仕事は、GolangとAngularを使用して安全なアプリケーションを開発するためのアーキテクチャとコードを提案することです。" + }, + { + "name": "フロントエンド開発", + "content": "上級フロントエンド開発者として機能してください。私はプロジェクトの詳細を説明し、" + "これらのツールを使用してプロジェクトをコーディングします。" + "Create React App、yarn、Ant Design、List、Redux Toolkit、createSlice、thunk、axiosを使用してください。" + "ファイルをindex.jsファイルに統合し、他のファイルではなく、説明は書かないでください。" + }, + { + "name": "ITアーキテクト", + "content": "ITアーキテクトの役割を果たしてください。私はアプリケーションや他のデジタル製品の機能に関する詳細を提供します。" + "あなたの仕事は、それをIT環境に統合する方法を考えることです。これには、ビジネス要件の分析、ギャップ分析の実施、" + "新システムの機能を既存のIT環境にマッピングすることが含まれる場合があります。次のステップは、" + "ソリューションデザインの作成、物理的なネットワークのブループリント、システム統合のインターフェース、およびデプロイメント環境のブループリントを定義することです。" + }, + { + "name": "コードインタープリター", + "content": "コードの解釈者として機能し、コードの文法と意味を明確に説明してください。" + }, + { + "name": "ITプログラミングの問題", + "content": "Stackoverflowの投稿として機能してください。私はプログラミングに関連する質問をします。" + "あなたは答えを何であるか答えます。十分な詳細がない場合は説明を書いてください。英語で何かを伝える必要があるときは、" + "大括弧でテキストを囲みます{このように}。" + }, + { + "name": "小红书風格", + "content": "Emojiスタイルで以下の段落を編集してください。このスタイルは、魅力的なタイトル、" + "各段落に絵文字を含め、関連するタグを末尾に追加することが特徴です。原文の意味を保持してください。" + }, + { + "name": "ライティングアシスタント", + "content": "中国語のライティング改善アシスタントとして、提供されたテキストのスペル、" + "文法、明瞭さ、簡潔さ、全体的な可読性を改善し、長い文を分解し、重複を減らし、" + "改善提案を提供します。テキストの修正版のみを提供し、説明は含めないでください。" + }, + { + "name": "Nature スタイルの編集", + "content": "プロのスペルと文法の校正者として機能し、私の記事を改善してください。" + "私の簡素化されたA0レベルの単語や文章を、より美しく、優雅で、" + "高度な英語の単語や文章に置き換えて、文学的な要素を加え、「自然」誌スタイルで表現レベルを高めてください。" + }, + ] + } + + @classmethod + def get_prompts(cls, lang: str) -> list: + return cls.DEFAULT.get(lang[:2], 'zh') diff --git a/apps/settings/serializers/__init__.py b/apps/settings/serializers/__init__.py index fe94eb1da..600684387 100644 --- a/apps/settings/serializers/__init__.py +++ b/apps/settings/serializers/__init__.py @@ -8,6 +8,7 @@ from .feature import * from .msg import * from .msg import * from .other import * +from .prompt import * from .public import * from .security import * from .settings import * diff --git a/apps/settings/serializers/auth/__init__.py b/apps/settings/serializers/auth/__init__.py index aeca390ac..2b641114c 100644 --- a/apps/settings/serializers/auth/__init__.py +++ b/apps/settings/serializers/auth/__init__.py @@ -11,3 +11,4 @@ from .saml2 import * from .sms import * from .sso import * from .wecom import * +from .slack import * diff --git a/apps/settings/serializers/auth/base.py b/apps/settings/serializers/auth/base.py index 57034e730..fbc833124 100644 --- a/apps/settings/serializers/auth/base.py +++ b/apps/settings/serializers/auth/base.py @@ -17,7 +17,8 @@ class AuthSettingSerializer(serializers.Serializer): AUTH_RADIUS = serializers.BooleanField(required=False, label=_('RADIUS Auth')) AUTH_DINGTALK = serializers.BooleanField(default=False, label=_('DingTalk Auth')) AUTH_FEISHU = serializers.BooleanField(default=False, label=_('FeiShu Auth')) - AUTH_WECOM = serializers.BooleanField(default=False, label=_('WeCom Auth')) + AUTH_WECOM = serializers.BooleanField(default=False, label=_('Slack Auth')) + AUTH_SLACK = serializers.BooleanField(default=False, label=_('WeCom Auth')) AUTH_SSO = serializers.BooleanField(default=False, label=_("SSO Auth")) AUTH_PASSKEY = serializers.BooleanField(default=False, label=_("Passkey Auth")) FORGOT_PASSWORD_URL = serializers.CharField( diff --git a/apps/settings/serializers/auth/slack.py b/apps/settings/serializers/auth/slack.py new file mode 100644 index 000000000..019137f4c --- /dev/null +++ b/apps/settings/serializers/auth/slack.py @@ -0,0 +1,15 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.serializers.fields import EncryptedField + +__all__ = ['SlackSettingSerializer'] + + +class SlackSettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Slack') + + AUTH_SLACK = serializers.BooleanField(default=False, label=_('Enable Slack Auth')) + SLACK_CLIENT_ID = serializers.CharField(max_length=256, required=True, label='Client ID') + SLACK_CLIENT_SECRET = EncryptedField(max_length=256, required=False, label='Client Secret') + SLACK_BOT_TOKEN = EncryptedField(max_length=256, required=False, label='Client bot Token') diff --git a/apps/settings/serializers/basic.py b/apps/settings/serializers/basic.py index ad1c2f8e3..4e253c602 100644 --- a/apps/settings/serializers/basic.py +++ b/apps/settings/serializers/basic.py @@ -1,6 +1,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from orgs.models import Organization + class BasicSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('Basic') @@ -34,3 +36,10 @@ class BasicSettingSerializer(serializers.Serializer): if not s: return 'http://127.0.0.1' return s.strip('/') + + @staticmethod + def validate_GLOBAL_ORG_DISPLAY_NAME(s): + org_names = Organization.objects.values_list('name', flat=True) + if s in org_names: + raise serializers.ValidationError(_('Organization name already exists')) + return s diff --git a/apps/settings/serializers/feature.py b/apps/settings/serializers/feature.py index f2efe5b03..8dc0de959 100644 --- a/apps/settings/serializers/feature.py +++ b/apps/settings/serializers/feature.py @@ -3,11 +3,13 @@ import uuid from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from assets.const import Protocol from common.serializers.fields import EncryptedField __all__ = [ 'AnnouncementSettingSerializer', 'OpsSettingSerializer', - 'VaultSettingSerializer', 'TicketSettingSerializer' + 'VaultSettingSerializer', 'TicketSettingSerializer', + 'ChatAISettingSerializer', 'VirtualAppSerializer', ] @@ -54,6 +56,44 @@ class VaultSettingSerializer(serializers.Serializer): ) +class ChatAISettingSerializer(serializers.Serializer): + PREFIX_TITLE = _('Chat AI') + GPT_MODEL_CHOICES = [] + + CHAT_AI_ENABLED = serializers.BooleanField( + required=False, label=_('Enable Chat AI') + ) + GPT_BASE_URL = serializers.CharField( + max_length=256, allow_blank=True, required=False, label=_('Base Url') + ) + GPT_API_KEY = EncryptedField( + max_length=256, allow_blank=True, required=False, label=_('API Key'), + ) + GPT_PROXY = serializers.CharField( + max_length=256, allow_blank=True, required=False, label=_('Proxy') + ) + GPT_MODEL = serializers.ChoiceField( + default='', choices=GPT_MODEL_CHOICES, label=_("GPT Model"), required=False, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_GPT_MODEL_choices() + + def set_GPT_MODEL_choices(self): + field_gpt_model = self.fields.get("GPT_MODEL") + if not field_gpt_model: + return + gpt_api_model = Protocol.gpt_protocols()[Protocol.chatgpt]['setting']['api_mode'] + choices = gpt_api_model['choices'] + field_gpt_model._choices = choices + field_gpt_model.default = gpt_api_model['default'] + cls = self.__class__ + if cls.GPT_MODEL_CHOICES: + return + cls.GPT_MODEL_CHOICES.extend(choices) + + class TicketSettingSerializer(serializers.Serializer): PREFIX_TITLE = _('Ticket') @@ -80,3 +120,11 @@ class OpsSettingSerializer(serializers.Serializer): label=_('Operation center command blacklist'), help_text=_("Commands that are not allowed execute.") ) + + +class VirtualAppSerializer(serializers.Serializer): + PREFIX_TITLE = _('Virtual app') + + VIRTUAL_APP_ENABLED = serializers.BooleanField( + required=False, label=_('Enable virtual app'), + ) diff --git a/apps/settings/serializers/prompt.py b/apps/settings/serializers/prompt.py new file mode 100644 index 000000000..6b7804516 --- /dev/null +++ b/apps/settings/serializers/prompt.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from settings.models import ChatPrompt + + +class ChatPromptSerializer(serializers.ModelSerializer): + class Meta: + model = ChatPrompt + fields = [ + 'id', 'name', 'content', 'builtin' + ] diff --git a/apps/settings/serializers/public.py b/apps/settings/serializers/public.py index 1b5c78609..6e5a74c8e 100644 --- a/apps/settings/serializers/public.py +++ b/apps/settings/serializers/public.py @@ -53,6 +53,10 @@ class PrivateSettingSerializer(PublicSettingSerializer): CONNECTION_TOKEN_REUSABLE = serializers.BooleanField() CACHE_LOGIN_PASSWORD_ENABLED = serializers.BooleanField() VAULT_ENABLED = serializers.BooleanField() + VIRTUAL_APP_ENABLED = serializers.BooleanField() + CHAT_AI_ENABLED = serializers.BooleanField() + GPT_MODEL = serializers.CharField() + FILE_UPLOAD_SIZE_LIMIT_MB = serializers.IntegerField() class ServerInfoSerializer(serializers.Serializer): diff --git a/apps/settings/urls/api_urls.py b/apps/settings/urls/api_urls.py index 97055bc83..fdfae5146 100644 --- a/apps/settings/urls/api_urls.py +++ b/apps/settings/urls/api_urls.py @@ -1,24 +1,26 @@ from __future__ import absolute_import from django.urls import path +from rest_framework_bulk.routes import BulkRouter from .. import api app_name = 'common' +router = BulkRouter() +router.register(r'chatai-prompts', api.ChatPromptViewSet, 'chatai-prompt') urlpatterns = [ path('mail/testing/', api.MailTestingAPI.as_view(), name='mail-testing'), - path('ldap/testing/config/', api.LDAPTestingConfigAPI.as_view(), name='ldap-testing-config'), - path('ldap/testing/login/', api.LDAPTestingLoginAPI.as_view(), name='ldap-testing-login'), path('ldap/users/', api.LDAPUserListApi.as_view(), name='ldap-user-list'), path('ldap/users/import/', api.LDAPUserImportAPI.as_view(), name='ldap-user-import'), - path('ldap/cache/refresh/', api.LDAPCacheRefreshAPI.as_view(), name='ldap-cache-refresh'), path('wecom/testing/', api.WeComTestingAPI.as_view(), name='wecom-testing'), path('dingtalk/testing/', api.DingTalkTestingAPI.as_view(), name='dingtalk-testing'), path('feishu/testing/', api.FeiShuTestingAPI.as_view(), name='feishu-testing'), + path('slack/testing/', api.SlackTestingAPI.as_view(), name='slack-testing'), path('sms//testing/', api.SMSTestingAPI.as_view(), name='sms-testing'), path('sms/backend/', api.SMSBackendAPI.as_view(), name='sms-backend'), path('vault/testing/', api.VaultTestingAPI.as_view(), name='vault-testing'), + path('chatai/testing/', api.ChatAITestingAPI.as_view(), name='chatai-testing'), path('vault/sync/', api.VaultSyncDataAPI.as_view(), name='vault-sync'), path('security/block-ip/', api.BlockIPSecurityAPI.as_view(), name='block-ip'), path('security/unlock-ip/', api.UnlockIPSecurityAPI.as_view(), name='unlock-ip'), @@ -29,3 +31,5 @@ urlpatterns = [ path('public/open/', api.OpenPublicSettingApi.as_view(), name='open-public-setting'), path('server-info/', api.ServerInfoApi.as_view(), name='server-info'), ] + +urlpatterns += router.urls diff --git a/apps/settings/urls/ws_urls.py b/apps/settings/urls/ws_urls.py index b1555c957..7e8ae6100 100644 --- a/apps/settings/urls/ws_urls.py +++ b/apps/settings/urls/ws_urls.py @@ -6,4 +6,5 @@ app_name = 'common' urlpatterns = [ path('ws/setting/tools/', ws.ToolsWebsocket.as_asgi(), name='setting-tools-ws'), + path('ws/ldap/', ws.LdapWebsocket.as_asgi(), name='ldap-ws'), ] diff --git a/apps/settings/utils/ldap.py b/apps/settings/utils/ldap.py index d5fba8ce9..23b7b3882 100644 --- a/apps/settings/utils/ldap.py +++ b/apps/settings/utils/ldap.py @@ -277,6 +277,9 @@ class LDAPCacheUtil(object): class LDAPSyncUtil(object): + class LDAPSyncUtilException(Exception): + pass + CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG = 'CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG' CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS = 'CACHE_KEY_LDAP_USERS_SYNC_TASK_STATUS' @@ -328,34 +331,29 @@ class LDAPSyncUtil(object): def get_task_error_msg(self): logger.info('Get task error msg') - error_msg = cache.get(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG) + error_msg = cache.get(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG, '') return error_msg def delete_task_error_msg(self): logger.info('Delete task error msg') cache.delete(self.CACHE_KEY_LDAP_USERS_SYNC_TASK_ERROR_MSG) - def pre_sync(self): - self.set_task_status(self.TASK_STATUS_IS_RUNNING) - def sync(self): users = self.server_util.search() self.cache_util.set_users(users) - def post_sync(self): - self.set_task_status(self.TASK_STATUS_IS_OVER) - def perform_sync(self): logger.info('Start perform sync ldap users from server to cache') try: - self.pre_sync() + ok, msg = LDAPTestUtil().test_config() + if not ok: + raise self.LDAPSyncUtilException(msg) self.sync() except Exception as e: error_msg = str(e) logger.error(error_msg) self.set_task_error_msg(error_msg) finally: - self.post_sync() logger.info('End perform sync ldap users from server to cache') close_old_connections() @@ -650,9 +648,9 @@ class LDAPTestUtil(object): # test login def _test_before_login_check(self, username, password): - ok, msg = self.test_config() - if not ok: - raise LDAPConfigurationError(msg) + from settings.ws import CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS, TASK_STATUS_IS_OVER + if not cache.get(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS): + raise self.LDAPBeforeLoginCheckError(_('Please test the connection first')) backend = LDAPAuthorizationBackend() ok, msg = backend.pre_check(username, password) diff --git a/apps/settings/ws.py b/apps/settings/ws.py index a8ae2c398..0f8f344fe 100644 --- a/apps/settings/ws.py +++ b/apps/settings/ws.py @@ -1,19 +1,38 @@ # -*- coding: utf-8 -*- # import json +import asyncio from channels.generic.websocket import AsyncJsonWebsocketConsumer +from django.core.cache import cache +from django.conf import settings from common.db.utils import close_old_connections from common.utils import get_logger +from settings.serializers import ( + LDAPTestConfigSerializer, + LDAPTestLoginSerializer +) +from settings.tasks import sync_ldap_user +from settings.utils import ( + LDAPSyncUtil, LDAPTestUtil +) from .tools import ( verbose_ping, verbose_telnet, verbose_nmap, verbose_tcpdump, verbose_traceroute ) - logger = get_logger(__name__) +CACHE_KEY_LDAP_TEST_CONFIG_MSG = 'CACHE_KEY_LDAP_TEST_CONFIG_MSG' +CACHE_KEY_LDAP_TEST_LOGIN_MSG = 'CACHE_KEY_LDAP_TEST_LOGIN_MSG' +CACHE_KEY_LDAP_SYNC_USER_MSG = 'CACHE_KEY_LDAP_SYNC_USER_MSG' +CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS = 'CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS' +CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS = 'CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS' +CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS = 'CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS' +TASK_STATUS_IS_RUNNING = 'RUNNING' +TASK_STATUS_IS_OVER = 'OVER' + class ToolsWebsocket(AsyncJsonWebsocketConsumer): @@ -60,7 +79,7 @@ class ToolsWebsocket(AsyncJsonWebsocketConsumer): logger.info(f'Receive request tcpdump: {params}') await verbose_tcpdump(display=self.send_msg, **params) - async def imitate_traceroute(self,dest_ips): + async def imitate_traceroute(self, dest_ips): params = {'dest_ips': dest_ips} await verbose_traceroute(display=self.send_msg, **params) @@ -78,3 +97,113 @@ class ToolsWebsocket(AsyncJsonWebsocketConsumer): async def disconnect(self, code): await self.close() close_old_connections() + + +class LdapWebsocket(AsyncJsonWebsocketConsumer): + async def connect(self): + user = self.scope["user"] + if user.is_authenticated: + await self.accept() + else: + await self.close() + + async def receive(self, text_data=None, bytes_data=None, **kwargs): + data = json.loads(text_data) + msg_type = data.pop('msg_type', 'testing_config') + try: + tool_func = getattr(self, f'run_{msg_type.lower()}') + await asyncio.to_thread(tool_func, data) + if msg_type == 'testing_config': + ok, msg = cache.get(CACHE_KEY_LDAP_TEST_CONFIG_MSG) + elif msg_type == 'sync_user': + ok, msg = cache.get(CACHE_KEY_LDAP_SYNC_USER_MSG) + else: + ok, msg = cache.get(CACHE_KEY_LDAP_TEST_LOGIN_MSG) + await self.send_msg(ok, msg) + except Exception as error: + await self.send_msg(msg='Exception: %s' % error) + + async def send_msg(self, ok=True, msg=''): + await self.send_json({'ok': ok, 'msg': f'{msg}'}) + + async def disconnect(self, code): + await self.close() + close_old_connections() + + @staticmethod + def get_ldap_config(serializer): + server_uri = serializer.validated_data["AUTH_LDAP_SERVER_URI"] + bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] + password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] + use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) + search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] + search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] + attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] + auth_ldap = serializer.validated_data.get('AUTH_LDAP', False) + + if not password: + password = settings.AUTH_LDAP_BIND_PASSWORD + + config = { + 'server_uri': server_uri, + 'bind_dn': bind_dn, + 'password': password, + 'use_ssl': use_ssl, + 'search_ou': search_ou, + 'search_filter': search_filter, + 'attr_map': attr_map, + 'auth_ldap': auth_ldap + } + return config + + @staticmethod + def task_is_over(task_key): + return cache.get(task_key) == TASK_STATUS_IS_OVER + + @staticmethod + def set_task_status_over(task_key, ttl=120): + cache.set(task_key, TASK_STATUS_IS_OVER, ttl) + + @staticmethod + def set_task_msg(task_key, ok, msg): + cache.set(task_key, (ok, msg), 120) + + def run_testing_config(self, data): + while True: + if self.task_is_over(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS): + break + else: + serializer = LDAPTestConfigSerializer(data=data) + if not serializer.is_valid(): + self.send_msg(msg=f'error: {str(serializer.errors)}') + config = self.get_ldap_config(serializer) + ok, msg = LDAPTestUtil(config).test_config() + self.set_task_status_over(CACHE_KEY_LDAP_TEST_CONFIG_TASK_STATUS) + self.set_task_msg(CACHE_KEY_LDAP_TEST_CONFIG_MSG, ok, msg) + + def run_testing_login(self, data): + while True: + if self.task_is_over(CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS): + break + else: + serializer = LDAPTestLoginSerializer(data=data) + if not serializer.is_valid(): + self.send_msg(msg=f'error: {str(serializer.errors)}') + username = serializer.validated_data['username'] + password = serializer.validated_data['password'] + ok, msg = LDAPTestUtil().test_login(username, password) + self.set_task_status_over(CACHE_KEY_LDAP_TEST_LOGIN_TASK_STATUS, 3) + self.set_task_msg(CACHE_KEY_LDAP_TEST_LOGIN_MSG, ok, msg) + + def run_sync_user(self, data): + while True: + if self.task_is_over(CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS): + break + else: + sync_util = LDAPSyncUtil() + sync_util.clear_cache() + sync_ldap_user() + msg = sync_util.get_task_error_msg() + ok = False if msg else True + self.set_task_status_over(CACHE_KEY_LDAP_SYNC_USER_TASK_STATUS) + self.set_task_msg(CACHE_KEY_LDAP_SYNC_USER_MSG, ok, msg) diff --git a/apps/static/img/beian.png b/apps/static/img/beian.png new file mode 100644 index 000000000..6fe667f73 Binary files /dev/null and b/apps/static/img/beian.png differ diff --git a/apps/static/img/login_slack_logo.png b/apps/static/img/login_slack_logo.png new file mode 100644 index 000000000..4d0797eda Binary files /dev/null and b/apps/static/img/login_slack_logo.png differ diff --git a/apps/templates/_base_double_screen.html b/apps/templates/_base_double_screen.html index d0281fc84..a87755592 100644 --- a/apps/templates/_base_double_screen.html +++ b/apps/templates/_base_double_screen.html @@ -22,20 +22,14 @@ -

-
-
- {% block content %} {% endblock %} -
-
-
-
-
- {% include '_copyright.html' %} -
+
+
+
+ {% block content %} {% endblock %}
+
+
-{% include '_foot_js.html' %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/templates/_foot_js.html b/apps/templates/_foot_js.html index 02c0eb5ac..0362e0253 100644 --- a/apps/templates/_foot_js.html +++ b/apps/templates/_foot_js.html @@ -10,13 +10,42 @@ +{% if INTERFACE.beian_text %} + + +{% endif %} diff --git a/apps/templates/resource_download.html b/apps/templates/resource_download.html index e1e49f124..63300b28f 100644 --- a/apps/templates/resource_download.html +++ b/apps/templates/resource_download.html @@ -15,7 +15,7 @@ p {
-

JumpServer {% trans 'Client' %} v2.0.2

+

JumpServer {% trans 'Client' %} v2.1.0

{% trans 'JumpServer Client, currently used to launch the client, now only support launch RDP SSH client, The Telnet client will next' %}

diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py index c4a60efb6..b6afedd51 100644 --- a/apps/terminal/api/__init__.py +++ b/apps/terminal/api/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # -from .session import * -from .component import * from .applet import * +from .component import * from .db_listen_port import * +from .session import * +from .virtualapp import * diff --git a/apps/terminal/api/applet/applet.py b/apps/terminal/api/applet/applet.py index 9e656487b..7f68d67d3 100644 --- a/apps/terminal/api/applet/applet.py +++ b/apps/terminal/api/applet/applet.py @@ -1,4 +1,6 @@ +import os import os.path +import re import shutil import zipfile from typing import Callable @@ -51,6 +53,11 @@ class DownloadUploadMixin: raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)}) tmp_dir = os.path.join(extract_to, file.name.replace('.zip', '')) + if not os.path.exists(tmp_dir): + name = file.name + name = re.match(r"(\w+)", name).group() + tmp_dir = os.path.join(extract_to, name) + manifest = Applet.validate_pkg(tmp_dir) return manifest, tmp_dir diff --git a/apps/terminal/api/session/command.py b/apps/terminal/api/session/command.py index 494d001c0..baec8f0fa 100644 --- a/apps/terminal/api/session/command.py +++ b/apps/terminal/api/session/command.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import uuid from django.utils import timezone from rest_framework import generics from rest_framework.fields import DateTimeField diff --git a/apps/terminal/api/session/session.py b/apps/terminal/api/session/session.py index c286dee83..8865801ac 100644 --- a/apps/terminal/api/session/session.py +++ b/apps/terminal/api/session/session.py @@ -62,7 +62,7 @@ class SessionFilterSet(BaseFilterSet): class Meta: model = Session fields = [ - "user", "asset", "account", "remote_addr", + "user", "user_id", "asset", "asset_id", "account", "remote_addr", "protocol", "is_finished", 'login_from', 'terminal' ] diff --git a/apps/terminal/api/virtualapp/__init__.py b/apps/terminal/api/virtualapp/__init__.py new file mode 100644 index 000000000..0c67de93f --- /dev/null +++ b/apps/terminal/api/virtualapp/__init__.py @@ -0,0 +1,3 @@ +from .provider import * +from .relation import * +from .virtualapp import * diff --git a/apps/terminal/api/virtualapp/provider.py b/apps/terminal/api/virtualapp/provider.py new file mode 100644 index 000000000..fbdf11ecc --- /dev/null +++ b/apps/terminal/api/virtualapp/provider.py @@ -0,0 +1,63 @@ +from django.core.cache import cache +from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response + +from common.api import JMSBulkModelViewSet +from common.permissions import IsServiceAccount +from orgs.utils import tmp_to_builtin_org +from terminal.models import AppProvider +from terminal.serializers import ( + AppProviderSerializer, AppProviderContainerSerializer +) + +__all__ = ['AppProviderViewSet', ] + + +class AppProviderViewSet(JMSBulkModelViewSet): + serializer_class = AppProviderSerializer + queryset = AppProvider.objects.all() + search_fields = ['name', 'hostname', ] + rbac_perms = { + 'containers': 'terminal.view_appprovider', + 'status': 'terminal.view_appprovider', + } + + cache_status_key_prefix = 'virtual_host_{}_status' + + def dispatch(self, request, *args, **kwargs): + with tmp_to_builtin_org(system=1): + return super().dispatch(request, *args, **kwargs) + + def get_permissions(self): + if self.action == 'create': + return [IsServiceAccount()] + return super().get_permissions() + + def perform_create(self, serializer): + request_terminal = getattr(self.request.user, 'terminal', None) + if not request_terminal: + raise ValidationError('Request user has no terminal') + data = dict() + data['terminal'] = request_terminal + data['id'] = self.request.user.id + serializer.save(**data) + + @action(detail=True, methods=['get'], serializer_class=AppProviderContainerSerializer) + def containers(self, request, *args, **kwargs): + instance = self.get_object() + key = self.cache_status_key_prefix.format(instance.id) + data = cache.get(key) + if not data: + data = [] + return self.get_paginated_response_from_queryset(data) + + @action(detail=True, methods=['post'], serializer_class=AppProviderContainerSerializer) + def status(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data, many=True) + serializer.is_valid(raise_exception=True) + validated_data = serializer.validated_data + key = self.cache_status_key_prefix.format(instance.id) + cache.set(key, validated_data, 60 * 3) + return Response({'msg': 'ok'}) diff --git a/apps/terminal/api/virtualapp/relation.py b/apps/terminal/api/virtualapp/relation.py new file mode 100644 index 000000000..9c15a0e0d --- /dev/null +++ b/apps/terminal/api/virtualapp/relation.py @@ -0,0 +1,64 @@ +from typing import Callable + +from django.conf import settings +from django.shortcuts import get_object_or_404 +from rest_framework.request import Request + +from common.api import JMSModelViewSet +from common.permissions import IsServiceAccount +from common.utils import is_uuid +from rbac.permissions import RBACPermission +from terminal.models import AppProvider +from terminal.serializers import ( + VirtualAppPublicationSerializer +) + + +class ProviderMixin: + request: Request + permission_denied: Callable + kwargs: dict + rbac_perms = ( + ('list', 'terminal.view_appprovider'), + ('retrieve', 'terminal.view_appprovider'), + ) + + def get_permissions(self): + if self.kwargs.get('host') and settings.DEBUG: + return [RBACPermission()] + else: + return [IsServiceAccount()] + + def self_provider(self): + try: + return self.request.user.terminal.app_provider + except AttributeError: + raise self.permission_denied(self.request, 'User has no app provider') + + def pk_provider(self): + return get_object_or_404(AppProvider, id=self.kwargs.get('provider')) + + @property + def provider(self): + if self.kwargs.get('provider'): + host = self.pk_provider() + else: + host = self.self_provider() + return host + + +class AppProviderAppViewSet(ProviderMixin, JMSModelViewSet): + provider: AppProvider + serializer_class = VirtualAppPublicationSerializer + filterset_fields = ['provider__name', 'app__name', 'status'] + + def get_object(self): + pk = self.kwargs.get('pk') + if not is_uuid(pk): + return self.provider.publications.get(app__name=pk) + else: + return self.provider.publications.get(id=pk) + + def get_queryset(self): + queryset = self.provider.publications.all() + return queryset diff --git a/apps/terminal/api/virtualapp/virtualapp.py b/apps/terminal/api/virtualapp/virtualapp.py new file mode 100644 index 000000000..7b4c5b9b2 --- /dev/null +++ b/apps/terminal/api/virtualapp/virtualapp.py @@ -0,0 +1,77 @@ +import os.path +import shutil +import zipfile +from typing import Callable + +from django.core.files.storage import default_storage +from django.utils._os import safe_join +from django.utils.translation import gettext as _ +from rest_framework import viewsets +from rest_framework.decorators import action +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import ValidationError + +from common.api import JMSBulkModelViewSet +from common.serializers import FileSerializer +from terminal import serializers +from terminal.models import VirtualAppPublication, VirtualApp + +__all__ = ['VirtualAppViewSet', 'VirtualAppPublicationViewSet'] + + +class UploadMixin: + get_serializer: Callable + request: Request + get_object: Callable + + def extract_zip_pkg(self): + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + file = serializer.validated_data['file'] + save_to = 'virtual_apps/{}'.format(file.name + '.tmp.zip') + if default_storage.exists(save_to): + default_storage.delete(save_to) + rel_path = default_storage.save(save_to, file) + path = default_storage.path(rel_path) + extract_to = default_storage.path('virtual_apps/{}.tmp'.format(file.name)) + if os.path.exists(extract_to): + shutil.rmtree(extract_to) + try: + with zipfile.ZipFile(path) as zp: + if zp.testzip() is not None: + raise ValidationError({'error': _('Invalid zip file')}) + zp.extractall(extract_to) + except RuntimeError as e: + raise ValidationError({'error': _('Invalid zip file') + ': {}'.format(e)}) + tmp_dir = safe_join(extract_to, file.name.replace('.zip', '')) + return tmp_dir + + @action(detail=False, methods=['post'], serializer_class=FileSerializer) + def upload(self, request, *args, **kwargs): + tmp_dir = self.extract_zip_pkg() + manifest = VirtualApp.validate_pkg(tmp_dir) + name = manifest['name'] + instance = VirtualApp.objects.filter(name=name).first() + if instance: + return Response({'error': 'virtual app already exists: {}'.format(name)}, status=400) + + app, serializer = VirtualApp.install_from_dir(tmp_dir) + return Response(serializer.data, status=201) + + +class VirtualAppViewSet(UploadMixin, JMSBulkModelViewSet): + queryset = VirtualApp.objects.all() + serializer_class = serializers.VirtualAppSerializer + filterset_fields = ['name', 'is_active'] + search_fields = ['name', 'image_name'] + rbac_perms = { + 'upload': 'terminal.add_virtualapp', + } + + +class VirtualAppPublicationViewSet(viewsets.ModelViewSet): + queryset = VirtualAppPublication.objects.all() + serializer_class = serializers.VirtualAppPublicationSerializer + filterset_fields = ['app__name', 'provider__name', 'status'] + search_fields = ['app__name', 'provider__name', ] diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index 9c7ee989a..72007112d 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -49,6 +49,7 @@ class NativeClient(TextChoices): Protocol.mongodb: [cls.db_client, cls.db_guide], Protocol.oracle: [cls.db_client, cls.db_guide], Protocol.postgresql: [cls.db_client, cls.db_guide], + Protocol.sqlserver: [cls.db_client, cls.db_guide], } return clients @@ -113,6 +114,26 @@ class AppletMethod: return methods +class VirtualAppMethod: + + @classmethod + def get_methods(cls): + from .models import VirtualApp + methods = defaultdict(list) + if not getattr(settings, 'VIRTUAL_APP_ENABLED'): + return methods + virtual_apps = VirtualApp.objects.filter(is_active=True) + for virtual_app in virtual_apps: + for protocol in virtual_app.protocols: + methods[protocol].append({ + 'value': virtual_app.name, + 'label': virtual_app.name, + 'type': 'virtual_app', + 'disabled': not virtual_app.is_active, + }) + return methods + + class ConnectMethodUtil: _all_methods = {} @@ -160,7 +181,7 @@ class ConnectMethodUtil: 'support': [ Protocol.mysql, Protocol.postgresql, Protocol.oracle, Protocol.mariadb, - Protocol.redis + Protocol.redis, Protocol.sqlserver ], 'match': 'map' }, @@ -243,6 +264,7 @@ class ConnectMethodUtil: methods = defaultdict(list) spec_web_methods = WebMethod.get_spec_methods() applet_methods = AppletMethod.get_methods() + virtual_app_methods = VirtualAppMethod.get_methods() native_methods = NativeClient.get_methods(os=os) for component, component_protocol in cls.components().items(): @@ -295,5 +317,12 @@ class ConnectMethodUtil: method['component'] = TerminalType.tinker.value methods[asset_protocol].extend(applet_methods) + # 虚拟应用方式,这个只有 panda 提供,并且协议可能是自定义的 + for protocol, virtual_app_methods in virtual_app_methods.items(): + for method in virtual_app_methods: + method['listen'] = Protocol.http + method['component'] = TerminalType.panda.value + methods[protocol].extend(virtual_app_methods) + cls._all_methods[os] = methods return methods diff --git a/apps/terminal/const.py b/apps/terminal/const.py index a545a0a3a..8ad37e93d 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -66,6 +66,7 @@ class TerminalType(TextChoices): video_worker = 'video_worker', 'Video Worker' chen = 'chen', 'Chen' kael = 'kael', 'Kael' + panda = 'panda', 'Panda' @classmethod def types(cls): @@ -102,3 +103,4 @@ class SessionErrorReason(TextChoices): replay_create_failed = 'replay_create_failed', _('Replay create failed') replay_upload_failed = 'replay_upload_failed', _('Replay upload failed') replay_convert_failed = 'replay_convert_failed', _('Replay convert failed') + replay_unsupported = 'replay_unsupported', _('Replay unsupported') diff --git a/apps/terminal/filters.py b/apps/terminal/filters.py index ae11733cf..0839c770c 100644 --- a/apps/terminal/filters.py +++ b/apps/terminal/filters.py @@ -2,7 +2,7 @@ from django.db.models import QuerySet from django_filters import rest_framework as filters from orgs.utils import filter_org_queryset -from terminal.models import Command, CommandStorage +from terminal.models import Command, CommandStorage, Session class CommandFilter(filters.FilterSet): @@ -13,11 +13,12 @@ class CommandFilter(filters.FilterSet): user = filters.CharFilter(lookup_expr='startswith') input = filters.CharFilter(lookup_expr='icontains') asset = filters.CharFilter(field_name='asset', lookup_expr='icontains') + asset_id = filters.UUIDFilter(method='filter_by_asset_id') class Meta: model = Command fields = [ - 'asset', 'account', 'user', 'session', 'risk_level', 'input', + 'asset', 'asset_id', 'account', 'user', 'session', 'risk_level', 'input', 'date_from', 'date_to', 'session_id', 'risk_level', 'command_storage_id', ] @@ -47,6 +48,15 @@ class CommandFilter(filters.FilterSet): qs = qs.filter(**filters) return qs + def filter_by_asset_id(self, queryset, name, value): + asset_id = self.form.cleaned_data.get('asset_id') + filters = {} + if asset_id: + session_ids = Session.objects.filter(asset_id=asset_id).values_list('id', flat=True) + filters['session__in'] = list(session_ids) + queryset = queryset.filter(**filters) + return queryset + class CommandFilterForStorageTree(CommandFilter): asset = filters.CharFilter(method='do_nothing') diff --git a/apps/terminal/migrations/0068_virtualapp.py b/apps/terminal/migrations/0068_virtualapp.py new file mode 100644 index 000000000..1ac230620 --- /dev/null +++ b/apps/terminal/migrations/0068_virtualapp.py @@ -0,0 +1,94 @@ +# Generated by Django 4.1.10 on 2023-12-05 07:02 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0067_alter_replaystorage_type'), + ] + + operations = [ + migrations.CreateModel( + name='AppProvider', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), + ('hostname', models.CharField(max_length=128, verbose_name='Hostname')), + ], + options={ + 'ordering': ('-date_created',), + }, + ), + migrations.CreateModel( + name='VirtualApp', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), + ('display_name', models.CharField(max_length=128, verbose_name='Display name')), + ('version', models.CharField(max_length=16, verbose_name='Version')), + ('author', models.CharField(max_length=128, verbose_name='Author')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('protocols', models.JSONField(default=list, verbose_name='Protocol')), + ('image_name', models.CharField(max_length=128, verbose_name='Image name')), + ('image_protocol', models.CharField(default='vnc', max_length=16, verbose_name='Image protocol')), + ('image_port', models.IntegerField(default=5900, verbose_name='Image port')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('tags', models.JSONField(default=list, verbose_name='Tags')), + ], + options={ + 'verbose_name': 'Virtual app', + }, + ), + migrations.AlterField( + model_name='terminal', + name='type', + field=models.CharField(choices=[('koko', 'KoKo'), ('guacamole', 'Guacamole'), ('omnidb', 'OmniDB'), ('xrdp', 'Xrdp'), ('lion', 'Lion'), ('core', 'Core'), ('celery', 'Celery'), ('magnus', 'Magnus'), ('razor', 'Razor'), ('tinker', 'Tinker'), ('video_worker', 'Video Worker'), ('chen', 'Chen'), ('kael', 'Kael'), ('panda', 'Panda')], default='koko', max_length=64, verbose_name='type'), + ), + migrations.CreateModel( + name='VirtualAppPublication', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.virtualapp', verbose_name='Virtual app')), + ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.appprovider', verbose_name='App Provider')), + ], + options={ + 'verbose_name': 'Virtual app publication', + 'unique_together': {('provider', 'app')}, + }, + ), + migrations.AddField( + model_name='virtualapp', + name='providers', + field=models.ManyToManyField(through='terminal.VirtualAppPublication', to='terminal.appprovider', verbose_name='Providers'), + ), + migrations.AddField( + model_name='appprovider', + name='apps', + field=models.ManyToManyField(through='terminal.VirtualAppPublication', to='terminal.virtualapp', verbose_name='Virtual app'), + ), + migrations.AddField( + model_name='appprovider', + name='terminal', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='app_provider', to='terminal.terminal', verbose_name='Terminal'), + ), + ] diff --git a/apps/terminal/migrations/0069_endpoint_sqlserver_port_alter_appprovider_apps_and_more.py b/apps/terminal/migrations/0069_endpoint_sqlserver_port_alter_appprovider_apps_and_more.py new file mode 100644 index 000000000..f68cca188 --- /dev/null +++ b/apps/terminal/migrations/0069_endpoint_sqlserver_port_alter_appprovider_apps_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.10 on 2023-12-08 09:41 + +import common.db.fields +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0068_virtualapp'), + ] + + operations = [ + migrations.AddField( + model_name='endpoint', + name='sqlserver_port', + field=common.db.fields.PortField(default=14330, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SQLServer port'), + ), + ] diff --git a/apps/terminal/models/__init__.py b/apps/terminal/models/__init__.py index 268727394..56fbd8cd9 100644 --- a/apps/terminal/models/__init__.py +++ b/apps/terminal/models/__init__.py @@ -1,3 +1,4 @@ from .session import * from .component import * from .applet import * +from .virtualapp import * diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index 661078011..fd5934285 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -171,7 +171,7 @@ class Applet(JMSBaseModel): if not hosts: return None - spec_label = asset.labels.filter(name__in=['AppletHost', '发布机']).first() + spec_label = asset.labels.filter(label__name__in=['AppletHost', '发布机']).first() if spec_label: matched = [host for host in hosts if host.name == spec_label.value] if matched: @@ -223,7 +223,9 @@ class Applet(JMSBaseModel): accounts = valid_accounts.exclude(username__in=accounts_username_used) public_accounts = accounts.filter(username__startswith='jms_') if not public_accounts: - public_accounts = accounts.exclude(username__in=['Administrator', 'root']) + public_accounts = accounts \ + .exclude(username__in=['Administrator', 'root']) \ + .exclude(username__startswith='js_') account = self.random_select_prefer_account(user, host, public_accounts) return account @@ -297,10 +299,9 @@ class Applet(JMSBaseModel): res = { 'host': host, 'account': account, - 'lock_key': lock_key, - 'ttl': ttl + 'lock_key': lock_key } - logger.debug('Select host and account: {}'.format(res)) + logger.debug('Select host and account: {}-{}'.format(host.name, account.username)) return res def delete(self, using=None, keep_parents=False): diff --git a/apps/terminal/models/component/endpoint.py b/apps/terminal/models/component/endpoint.py index 9b73dbea0..d9d4cfab8 100644 --- a/apps/terminal/models/component/endpoint.py +++ b/apps/terminal/models/component/endpoint.py @@ -20,6 +20,7 @@ class Endpoint(JMSBaseModel): mariadb_port = PortField(default=33062, verbose_name=_('MariaDB port')) postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL port')) redis_port = PortField(default=63790, verbose_name=_('Redis port')) + sqlserver_port = PortField(default=14330, verbose_name=_('SQLServer port')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) @@ -81,10 +82,10 @@ class Endpoint(JMSBaseModel): instance = instance.get_asset() if not isinstance(instance, Asset): return None - values = instance.labels.filter(name='endpoint').values_list('value', flat=True) + values = instance.labels.filter(label__name='endpoint').values_list('label__value', flat=True) if not values: return None - endpoints = cls.objects.filter(name__in=values).order_by('-date_updated') + endpoints = cls.objects.filter(name__in=list(values)).order_by('-date_updated') for endpoint in endpoints: if endpoint.is_valid_for(instance, protocol): return endpoint @@ -131,5 +132,11 @@ class EndpointRule(JMSBaseModel): endpoint = Endpoint.get_or_create_default(request) if not endpoint.host and request: # 动态添加 current request host - endpoint.host = request.get_host().split(':')[0] + 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 diff --git a/apps/terminal/models/component/task.py b/apps/terminal/models/component/task.py index c69a89423..572167cf3 100644 --- a/apps/terminal/models/component/task.py +++ b/apps/terminal/models/component/task.py @@ -20,3 +20,6 @@ class Task(JMSBaseModel): class Meta: db_table = "terminal_task" verbose_name = _("Task") + + def __str__(self): + return str(dict(SessionTaskChoices.choices).get(self.name)) diff --git a/apps/terminal/models/component/terminal.py b/apps/terminal/models/component/terminal.py index 9955abf03..a76d67dc6 100644 --- a/apps/terminal/models/component/terminal.py +++ b/apps/terminal/models/component/terminal.py @@ -117,6 +117,15 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel): from settings.utils import get_login_title return {'TERMINAL_HEADER_TITLE': get_login_title()} + @staticmethod + def get_chat_ai_setting(): + return { + 'GPT_BASE_URL': settings.GPT_BASE_URL, + 'GPT_API_KEY': settings.GPT_API_KEY, + 'GPT_PROXY': settings.GPT_PROXY, + 'GPT_MODEL': settings.GPT_MODEL, + } + @property def config(self): configs = {} @@ -127,6 +136,7 @@ class Terminal(StorageMixin, TerminalStatusMixin, JMSBaseModel): configs.update(self.get_command_storage_setting()) configs.update(self.get_replay_storage_setting()) configs.update(self.get_login_title_setting()) + configs.update(self.get_chat_ai_setting()) configs.update({ 'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME, 'SECURITY_SESSION_SHARE': settings.SECURITY_SESSION_SHARE, diff --git a/apps/terminal/models/virtualapp/__init__.py b/apps/terminal/models/virtualapp/__init__.py new file mode 100644 index 000000000..f784f5c73 --- /dev/null +++ b/apps/terminal/models/virtualapp/__init__.py @@ -0,0 +1,2 @@ +from .provider import * +from .virtualapp import * diff --git a/apps/terminal/models/virtualapp/provider.py b/apps/terminal/models/virtualapp/provider.py new file mode 100644 index 000000000..8642ff065 --- /dev/null +++ b/apps/terminal/models/virtualapp/provider.py @@ -0,0 +1,28 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from common.db.models import JMSBaseModel + +__all__ = ['AppProvider', ] + + +class AppProvider(JMSBaseModel): + name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) + hostname = models.CharField(max_length=128, verbose_name=_('Hostname')) + terminal = models.OneToOneField( + 'terminal.Terminal', on_delete=models.CASCADE, null=True, blank=True, + related_name='app_provider', verbose_name=_('Terminal') + ) + apps = models.ManyToManyField( + 'VirtualApp', verbose_name=_('Virtual app'), + through='VirtualAppPublication', through_fields=('provider', 'app'), + ) + + class Meta: + ordering = ('-date_created',) + + @property + def load(self): + if not self.terminal: + return 'offline' + return self.terminal.load diff --git a/apps/terminal/models/virtualapp/virtualapp.py b/apps/terminal/models/virtualapp/virtualapp.py new file mode 100644 index 000000000..877dea387 --- /dev/null +++ b/apps/terminal/models/virtualapp/virtualapp.py @@ -0,0 +1,103 @@ +import os +import shutil + +from django.conf import settings +from django.core.files.storage import default_storage +from django.db import models +from django.utils._os import safe_join +from django.utils.translation import gettext_lazy as _ +from rest_framework.serializers import ValidationError + +from common.db.models import JMSBaseModel +from common.utils import lazyproperty +from common.utils.yml import yaml_load_with_i18n + +__all__ = ['VirtualApp', 'VirtualAppPublication'] + + +class VirtualApp(JMSBaseModel): + name = models.SlugField(max_length=128, verbose_name=_('Name'), unique=True) + display_name = models.CharField(max_length=128, verbose_name=_('Display name')) + version = models.CharField(max_length=16, verbose_name=_('Version')) + author = models.CharField(max_length=128, verbose_name=_('Author')) + is_active = models.BooleanField(default=True, verbose_name=_('Is active')) + protocols = models.JSONField(default=list, verbose_name=_('Protocol')) + image_name = models.CharField(max_length=128, verbose_name=_('Image name')) + image_protocol = models.CharField(max_length=16, default='vnc', verbose_name=_('Image protocol')) + image_port = models.IntegerField(default=5900, verbose_name=_('Image port')) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) + tags = models.JSONField(default=list, verbose_name=_('Tags')) + providers = models.ManyToManyField( + through_fields=('app', 'provider',), through='VirtualAppPublication', + to='AppProvider', verbose_name=_('Providers') + ) + + class Meta: + verbose_name = _('Virtual app') + + def __str__(self): + return self.name + + @property + def path(self): + return default_storage.path('virtual_apps/{}'.format(self.name)) + + @lazyproperty + def readme(self): + readme_file = os.path.join(self.path, 'README.md') + if os.path.isfile(readme_file): + with open(readme_file, 'r') as f: + return f.read() + return '' + + @property + def icon(self): + path = os.path.join(self.path, 'icon.png') + if not os.path.exists(path): + return None + return os.path.join(settings.MEDIA_URL, 'virtual_apps', self.name, 'icon.png') + + @staticmethod + def validate_pkg(d): + files = ['manifest.yml', 'icon.png', ] + for name in files: + path = safe_join(d, name) + if not os.path.exists(path): + raise ValidationError({'error': _('Applet pkg not valid, Missing file {}').format(name)}) + + with open(safe_join(d, 'manifest.yml'), encoding='utf8') as f: + manifest = yaml_load_with_i18n(f) + + if not manifest.get('name', ''): + raise ValidationError({'error': 'Missing name in manifest.yml'}) + return manifest + + @classmethod + def install_from_dir(cls, path): + from terminal.serializers import VirtualAppSerializer + manifest = cls.validate_pkg(path) + name = manifest['name'] + instance = cls.objects.filter(name=name).first() + serializer = VirtualAppSerializer(instance=instance, data=manifest) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + + pkg_path = default_storage.path('virtual_apps/{}'.format(name)) + if os.path.exists(pkg_path): + shutil.rmtree(pkg_path) + shutil.copytree(path, pkg_path) + return instance, serializer + + +class VirtualAppPublication(JMSBaseModel): + provider = models.ForeignKey( + 'AppProvider', on_delete=models.CASCADE, related_name='publications', verbose_name=_('App Provider') + ) + app = models.ForeignKey( + 'VirtualApp', on_delete=models.CASCADE, related_name='publications', verbose_name=_('Virtual app') + ) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) + + class Meta: + verbose_name = _('Virtual app publication') + unique_together = ('provider', 'app') diff --git a/apps/terminal/serializers/__init__.py b/apps/terminal/serializers/__init__.py index dc23362f9..ba97ae16d 100644 --- a/apps/terminal/serializers/__init__.py +++ b/apps/terminal/serializers/__init__.py @@ -9,3 +9,5 @@ from .sharing import * from .storage import * from .task import * from .terminal import * +from .virtualapp import * +from .virtualapp_provider import * diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py index e3e435d76..146cfd3bc 100644 --- a/apps/terminal/serializers/applet_host.py +++ b/apps/terminal/serializers/applet_host.py @@ -49,8 +49,20 @@ class DeployOptionsSerializer(serializers.Serializer): RDS_LicensingMode = serializers.ChoiceField(choices=LICENSE_MODE_CHOICES, default=2, label=_('RDS Licensing Mode')) RDS_fSingleSessionPerUser = serializers.ChoiceField(choices=SESSION_PER_USER, default=1, label=_("RDS Single Session Per User")) - RDS_MaxDisconnectionTime = serializers.IntegerField(default=60000, label=_("RDS Max Disconnection Time")) - RDS_RemoteAppLogoffTimeLimit = serializers.IntegerField(default=0, label=_("RDS Remote App Logoff Time Limit")) + RDS_MaxDisconnectionTime = serializers.IntegerField( + default=60000, label=_("RDS Max Disconnection Time (ms)"), + help_text=_( + 'Tips: Set the maximum duration for keeping a disconnected session active on the server (log off the ' + 'session after 60000 milliseconds).' + ) + ) + RDS_RemoteAppLogoffTimeLimit = serializers.IntegerField( + default=0, label=_("RDS Remote App Logoff Time Limit (ms)"), + help_text=_( + 'Tips: Set the logoff time for RemoteApp sessions after closing all RemoteApp programs (0 milliseconds, ' + 'log off the session immediately).' + ) + ) class AppletHostSerializer(HostSerializer): diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index 82de09fce..2b734f71e 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -28,7 +28,7 @@ class EndpointSerializer(BulkModelSerializer): fields_small = [ 'host', 'https_port', 'http_port', 'ssh_port', 'rdp_port', 'mysql_port', 'mariadb_port', 'postgresql_port', 'redis_port', - 'oracle_port_range', 'oracle_port', + 'oracle_port_range', 'oracle_port', 'sqlserver_port', ] fields = fields_mini + fields_small + [ 'comment', 'date_created', 'date_updated', 'created_by' diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index caf887f21..85f52c609 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -281,3 +281,10 @@ class ReplayStorageSerializer(BaseStorageSerializer): extra_kwargs = { 'name': {'validators': [UniqueValidator(queryset=ReplayStorage.objects.all())]} } + + def validate_is_default(self, value): + if self.initial_data.get('type') == const.ReplayStorageType.sftp.value: + # sftp不能设置为默认存储 + return False + else: + return value diff --git a/apps/terminal/serializers/virtualapp.py b/apps/terminal/serializers/virtualapp.py new file mode 100644 index 000000000..5f959e490 --- /dev/null +++ b/apps/terminal/serializers/virtualapp.py @@ -0,0 +1,41 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.const.choices import Status +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from terminal.const import PublishStatus +from ..models import VirtualApp, VirtualAppPublication, AppProvider + +__all__ = [ + 'VirtualAppSerializer', 'VirtualAppPublicationSerializer' +] + + +class VirtualAppSerializer(serializers.ModelSerializer): + icon = serializers.ReadOnlyField(label=_("Icon")) + image_protocol = serializers.CharField(max_length=16, default='vnc') + image_port = serializers.IntegerField(default=5900) + + class Meta: + model = VirtualApp + fields_mini = ['id', 'display_name', 'name', 'image_name', 'is_active'] + read_only_fields = [ + 'icon', 'readme', 'date_created', 'date_updated', + ] + fields = fields_mini + [ + 'version', 'author', 'image_protocol', 'image_port', + 'protocols', 'tags', 'comment', + ] + read_only_fields + + +class VirtualAppPublicationSerializer(serializers.ModelSerializer): + app = ObjectRelatedField(attrs=('id', 'name', 'image_name',), label=_("Virtual app"), + queryset=VirtualApp.objects.all()) + provider = ObjectRelatedField(queryset=AppProvider.objects.all(), label=_("App Provider")) + status = LabeledChoiceField(choices=PublishStatus.choices, label=_("Status"), default=Status.pending) + + class Meta: + model = VirtualAppPublication + fields_mini = ['id', 'provider', 'app'] + read_only_fields = ['date_created', 'date_updated'] + fields = fields_mini + ['status', 'comment'] + read_only_fields diff --git a/apps/terminal/serializers/virtualapp_provider.py b/apps/terminal/serializers/virtualapp_provider.py new file mode 100644 index 000000000..bfce5e081 --- /dev/null +++ b/apps/terminal/serializers/virtualapp_provider.py @@ -0,0 +1,31 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.serializers.fields import LabeledChoiceField +from terminal import const +from ..models import AppProvider + +__all__ = ['AppProviderSerializer', 'AppProviderContainerSerializer', ] + + +class AppProviderSerializer(serializers.ModelSerializer): + load = LabeledChoiceField( + read_only=True, label=_('Load status'), choices=const.ComponentLoad.choices, + ) + + class Meta: + model = AppProvider + field_mini = ['id', 'name', 'hostname'] + read_only_fields = [ + 'date_created', 'date_updated', + ] + fields = field_mini + ['load', 'terminal'] + read_only_fields + + +class AppProviderContainerSerializer(serializers.Serializer): + container_id = serializers.CharField(label=_('Container ID')) + container_image = serializers.CharField(label=_('Container Image')) + container_name = serializers.CharField(label=_('Container Name')) + container_status = serializers.CharField(label=_('Container Status')) + container_ports = serializers.ListField(child=serializers.CharField(), label=_('Container Ports')) + diff --git a/apps/terminal/signal_handlers/__init__.py b/apps/terminal/signal_handlers/__init__.py index bd61c885c..08a6ac407 100644 --- a/apps/terminal/signal_handlers/__init__.py +++ b/apps/terminal/signal_handlers/__init__.py @@ -3,3 +3,4 @@ from .db_port import * from .session import * from .session_sharing import * from .terminal import * +from .virtualapp import * diff --git a/apps/terminal/signal_handlers/virtualapp.py b/apps/terminal/signal_handlers/virtualapp.py new file mode 100644 index 000000000..37c4ed1a1 --- /dev/null +++ b/apps/terminal/signal_handlers/virtualapp.py @@ -0,0 +1,24 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from common.decorators import on_transaction_commit +from ..models import AppProvider, VirtualApp + + +@receiver(post_save, sender=AppProvider) +@on_transaction_commit +def on_virtual_host_create(sender, instance, created=False, **kwargs): + if not created: + return + apps = VirtualApp.objects.all() + instance.apps.set(apps) + + +@receiver(post_save, sender=VirtualApp) +def on_virtual_app_create(sender, instance, created=False, **kwargs): + if not created: + return + providers = AppProvider.objects.all() + if len(providers) == 0: + return + instance.providers.set(providers) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 3c38bb934..258e2f0d1 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -30,6 +30,10 @@ router.register(r'applet-hosts', api.AppletHostViewSet, 'applet-host') router.register(r'applet-publications', api.AppletPublicationViewSet, 'applet-publication') router.register(r'applet-host-deployments', api.AppletHostDeploymentViewSet, 'applet-host-deployment') router.register(r'db-listen-ports', api.DBListenPortViewSet, 'db-listen-ports') +router.register(r'virtual-apps', api.VirtualAppViewSet, 'virtual-app') +router.register(r'app-providers', api.AppProviderViewSet, 'app-provider') +router.register(r'app-providers/((?P[^/.]+)/)?apps', api.AppProviderAppViewSet, 'app-provider-app') +router.register(r'virtual-app-publications', api.VirtualAppPublicationViewSet, 'virtual-app-publication') urlpatterns = [ path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'), diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index 91c0a047d..b3791b98b 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -75,6 +75,8 @@ class BaseTicketMessage(UserMessage): field = fields[name] item = {'name': name, 'title': field.verbose_name} value = self.ticket.get_field_display(name, field, data) + if not value: + continue item['value'] = value items.append(item) return items diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index edeb97ad2..607063367 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -41,7 +41,7 @@ class TicketSerializer(OrgResourceModelSerializerMixin): return choices = tp.choices choices.pop(TicketType.general, None) - tp.choices = choices + tp.choices = choices.items() @classmethod def setup_eager_loading(cls, queryset): diff --git a/apps/tickets/templates/tickets/_msg_ticket.html b/apps/tickets/templates/tickets/_msg_ticket.html index 75d205080..b781e1738 100644 --- a/apps/tickets/templates/tickets/_msg_ticket.html +++ b/apps/tickets/templates/tickets/_msg_ticket.html @@ -1,29 +1,31 @@ {% load i18n %} -
-

+

+

{{ title | safe }} -

+

{% for child in content %} -

{{ child.title }}

-
- {% for item in child.content %} -
  • - {{ item.title }}: {{ item.value }} -
  • - {% endfor %} +
    +

    {{ child.title }}

    + {% for item in child.content %} +

    + {{ item.title }}: + {{ item.value }} +

    + {% endfor %} +
    {% endfor %}
    -
    - diff --git a/apps/tickets/templates/tickets/approve_check_password.html b/apps/tickets/templates/tickets/approve_check_password.html index fe256bc1c..dabaaf7d8 100644 --- a/apps/tickets/templates/tickets/approve_check_password.html +++ b/apps/tickets/templates/tickets/approve_check_password.html @@ -5,55 +5,115 @@ {% block content %} -
    -
    -
    -

    {% trans 'Ticket information' %}

    -

    -
    +
    +
    +
    +

    {% trans 'Ticket information' %}

    {% for child in content %} -

    {{ child.title }}

    -
    - {% for item in child.content %} -
  • - {{ item.title }}: {{ item.value }} -
  • - {% endfor %} +
    +

    {{ child.title }}

    + {% for item in child.content %} +

    + {{ item.title }}: + {{ item.value }} +

    + {% endfor %} +
    {% endfor %}
    -
    -
    -
    - -

    {% trans 'Ticket approval' %}

    -

    +
    -

    - {% trans 'Hello' %} {{ user.name }}, -

    -

    - {{ prompt_msg }} -

    -
    -
    - {% csrf_token %} -
    - - -
    -
    + +

    {% trans 'Ticket approval' %}

    +

    +
    +

    + {% trans 'Hello' %} {{ user.name }}, +

    +

    + {{ prompt_msg }} +

    +
    +
    + {% csrf_token %} +
    + + +
    +
    +
    -
    - + .ticket-container { + flex-shrink: 0; + border-radius: 4px; + background: #FFF; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + + + .card { + .child_title { + padding-top: 16px; + margin: 0 0 12px 16px; + display: inline-flex; + flex-direction: column; + align-items: flex-start; + color: #1F2329; + font-size: 16px; + font-style: normal; + font-weight: 500; + } + + @media (max-width: 400px) { + margin: 8px + } + margin: 24px 0 0 24px; + width: 95%; + display: inline-block; + border-radius: 4px; + background: #F5F6F7; + } + + .card:last-child { + margin-bottom: 24px; + } + + .field-group { + font-size: 14px; + padding-inline-start: 0; + margin: 0; + width: 95%; + + .field-name { + margin: 4px 0 4px 16px; + color: #646A73; + display: inline-block; + + :is(strong) { + font-weight: 400 !important; + } + } + + .field-value { + color: #1F2329; + display: inline-block; + } + } + } + {% endblock %} diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 524571025..9c364ab75 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -52,10 +52,6 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, SuggestionMixin, BulkModelV 'bulk_remove': 'users.remove_user', } - def get_queryset(self): - queryset = super().get_queryset().prefetch_related('groups') - return queryset - def allow_bulk_destroy(self, qs, filtered): is_valid = filtered.count() < qs.count() if not is_valid: diff --git a/apps/users/const.py b/apps/users/const.py index a2ddb4ce9..38edeaf2d 100644 --- a/apps/users/const.py +++ b/apps/users/const.py @@ -38,6 +38,11 @@ class RDPSmartSize(TextChoices): ENABLE = '1', _('Enable') +class RDPColorQuality(TextChoices): + HIGH = '32', 'High(32 bit)' + MEDIUM = '16', 'Medium(16 bit)' + + class KeyboardLayout(TextChoices): EN_US_QWERTY = 'en-us-qwerty', 'US English (Qwerty)' EN_UK_QWERTY = 'en-gb-qwerty', 'UK English (Qwerty)' @@ -46,6 +51,8 @@ class KeyboardLayout(TextChoices): FR_CH_QWERTZ = 'fr-ch-qwertz', 'Swiss French (Qwertz)' FR_BE_AZERTY = 'fr-be-azerty', 'Belgian French (Azerty)' TR_TR_QWERTY = 'tr-tr-qwerty', 'Turkish-Q (Qwerty)' + ES_ES_QWERTY = 'es-es-qwerty', 'Spanish' + ES_LATAM_QWERTY = 'es-latam-qwerty', 'Spanish (Latin American)' class AppletConnectionMethod(TextChoices): diff --git a/apps/users/migrations/0044_usersession.py b/apps/users/migrations/0044_usersession.py index 325cd4f2c..9793eddd4 100644 --- a/apps/users/migrations/0044_usersession.py +++ b/apps/users/migrations/0044_usersession.py @@ -1,9 +1,10 @@ # Generated by Django 4.1.10 on 2023-09-14 07:23 +import uuid + +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion -import uuid class Migration(migrations.Migration): @@ -30,7 +31,7 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'User session', 'ordering': ['-date_created'], - 'permissions': [('offline_usersession', 'Offline ussr session')], + 'permissions': [('offline_usersession', 'Offline user session')], }, ), ] diff --git a/apps/users/migrations/0049_alter_user_unique_together_user_slack_id_and_more.py b/apps/users/migrations/0049_alter_user_unique_together_user_slack_id_and_more.py new file mode 100644 index 000000000..5847af8c2 --- /dev/null +++ b/apps/users/migrations/0049_alter_user_unique_together_user_slack_id_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.10 on 2023-11-23 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0048_wechat_phone_encrypt'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='user', + unique_together={('feishu_id',), ('dingtalk_id',), ('wecom_id',)}, + ), + migrations.AddField( + model_name='user', + name='slack_id', + field=models.CharField(default=None, max_length=128, null=True, verbose_name='Slack'), + ), + migrations.AlterField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius'), ('cas', 'CAS'), ('saml2', 'SAML2'), ('oauth2', 'OAuth2'), ('wecom', 'WeCom'), ('dingtalk', 'DingTalk'), ('feishu', 'FeiShu'), ('slack', 'Slack'), ('custom', 'Custom')], default='local', max_length=30, verbose_name='Source'), + ), + migrations.AlterUniqueTogether( + name='user', + unique_together={('slack_id',), ('feishu_id',), ('dingtalk_id',), ('wecom_id',)}, + ), + ] diff --git a/apps/users/models/group.py b/apps/users/models/group.py index 914c22f75..4f3f612d3 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -1,15 +1,15 @@ # -*- coding: utf-8 -*- - from django.db import models from django.utils.translation import gettext_lazy as _ from common.utils import lazyproperty +from labels.mixins import LabeledMixin from orgs.mixins.models import JMSOrgBaseModel __all__ = ['UserGroup'] -class UserGroup(JMSOrgBaseModel): +class UserGroup(LabeledMixin, JMSOrgBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name')) def __str__(self): diff --git a/apps/users/models/user.py b/apps/users/models/user.py index b8dd467c6..bf03dadce 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -24,6 +24,7 @@ from common.utils import ( date_expired_default, get_logger, lazyproperty, random_string, bulk_create_with_signal ) +from labels.mixins import LabeledMixin from orgs.utils import current_org from rbac.const import Scope from ..signals import ( @@ -40,6 +41,7 @@ class AuthMixin: history_passwords: models.Manager need_update_password: bool public_key: str + username: str is_local: bool set_password: Callable save: Callable @@ -62,9 +64,10 @@ class AuthMixin: def set_password(self, raw_password): if self.can_update_password(): - self.date_password_last_updated = timezone.now() - post_user_change_password.send(self.__class__, user=self) - super().set_password(raw_password) + if self.username: + self.date_password_last_updated = timezone.now() + post_user_change_password.send(self.__class__, user=self) + super().set_password(raw_password) # noqa def set_public_key(self, public_key): if self.can_update_ssh_key(): @@ -734,7 +737,7 @@ class JSONFilterMixin: return models.Q(id__in=user_id) -class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, AbstractUser): +class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, LabeledMixin, JSONFilterMixin, AbstractUser): class Source(models.TextChoices): local = 'local', _('Local') ldap = 'ldap', 'LDAP/AD' @@ -746,6 +749,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract wecom = 'wecom', _('WeCom') dingtalk = 'dingtalk', _('DingTalk') feishu = 'feishu', _('FeiShu') + slack = 'slack', _('Slack') custom = 'custom', 'Custom' SOURCE_BACKEND_MAPPING = { @@ -778,6 +782,9 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract Source.feishu: [ settings.AUTH_BACKEND_FEISHU ], + Source.slack: [ + settings.AUTH_BACKEND_SLACK + ], Source.dingtalk: [ settings.AUTH_BACKEND_DINGTALK ], @@ -848,12 +855,20 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract wecom_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('WeCom')) dingtalk_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('DingTalk')) feishu_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('FeiShu')) + slack_id = models.CharField(null=True, default=None, max_length=128, verbose_name=_('Slack')) DATE_EXPIRED_WARNING_DAYS = 5 def __str__(self): return '{0.name}({0.username})'.format(self) + @classmethod + def get_queryset(cls): + queryset = cls.objects.all() + if not current_org.is_root(): + queryset = current_org.get_members() + return queryset + @property def secret_key(self): instance = self.preferences.filter(name='secret_key').first() @@ -990,6 +1005,7 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, JSONFilterMixin, Abstract ('dingtalk_id',), ('wecom_id',), ('feishu_id',), + ('slack_id',), ) permissions = [ ('invite_user', _('Can invite user')), diff --git a/apps/users/serializers/group.py b/apps/users/serializers/group.py index af4e349e0..e05546bba 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -3,7 +3,8 @@ from django.db.models import Count from django.utils.translation import gettext_lazy as _ -from common.serializers.mixin import ObjectRelatedField +from common.serializers.fields import ObjectRelatedField +from common.serializers.mixin import ResourceLabelsMixin from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .. import utils from ..models import User, UserGroup @@ -13,7 +14,7 @@ __all__ = [ ] -class UserGroupSerializer(BulkOrgResourceModelSerializer): +class UserGroupSerializer(ResourceLabelsMixin, BulkOrgResourceModelSerializer): users = ObjectRelatedField( required=False, many=True, queryset=User.objects, label=_('User'), ) @@ -24,7 +25,7 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): fields_small = fields_mini + [ 'comment', 'date_created', 'created_by' ] - fields = fields_mini + fields_small + ['users'] + fields = fields_mini + fields_small + ['users', 'labels'] extra_kwargs = { 'created_by': {'label': _('Created by'), 'read_only': True}, 'users_amount': {'label': _('Users amount')}, @@ -43,5 +44,6 @@ class UserGroupSerializer(BulkOrgResourceModelSerializer): @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('users').annotate(users_amount=Count('users')) + queryset = queryset.prefetch_related('users', 'labels', 'labels__label') \ + .annotate(users_amount=Count('users')) return queryset diff --git a/apps/users/serializers/preference/luna.py b/apps/users/serializers/preference/luna.py index 2df89f7bc..b89bf4b36 100644 --- a/apps/users/serializers/preference/luna.py +++ b/apps/users/serializers/preference/luna.py @@ -5,7 +5,7 @@ from rest_framework import serializers from users.const import ( RDPResolution, RDPSmartSize, KeyboardLayout, - RDPClientOption, AppletConnectionMethod + RDPClientOption, AppletConnectionMethod, RDPColorQuality, ) @@ -40,6 +40,10 @@ class GraphicsSerializer(serializers.Serializer): choices=RDPClientOption.choices, default={RDPClientOption.FULL_SCREEN}, label=_('RDP client option'), required=False ) + rdp_color_quality = serializers.ChoiceField( + choices=RDPColorQuality.choices, default=RDPColorQuality.HIGH, + label=_('RDP color quality'), required=False + ) rdp_smart_size = serializers.ChoiceField( RDPSmartSize.choices, default=RDPSmartSize.DISABLE, required=False, label=_('Rdp smart size'), diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index 61869d95b..eacc0bff3 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -143,9 +143,9 @@ class UserProfileSerializer(UserSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) system_roles_field = self.fields.get('system_roles') - org_roles_field = self.fields.get('org_roles') if system_roles_field: system_roles_field.read_only = True + org_roles_field = self.fields.get('org_roles') if org_roles_field: org_roles_field.read_only = True diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 59ae59b52..d4228b920 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -6,7 +6,7 @@ from functools import partial from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.serializers import CommonBulkSerializerMixin +from common.serializers import CommonBulkSerializerMixin, ResourceLabelsMixin from common.serializers.fields import ( EncryptedField, ObjectRelatedField, LabeledChoiceField, PhoneField ) @@ -81,7 +81,7 @@ class RolesSerializerMixin(serializers.Serializer): return fields -class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializers.ModelSerializer): +class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, ResourceLabelsMixin, serializers.ModelSerializer): password_strategy = LabeledChoiceField( choices=PasswordStrategy.choices, default=PasswordStrategy.email, @@ -121,7 +121,7 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer # small 指的是 不需要计算的直接能从一张表中获取到的数据 fields_small = fields_mini + fields_write_only + [ "email", "wechat", "phone", "mfa_level", "source", - "wecom_id", "dingtalk_id", "feishu_id", + "wecom_id", "dingtalk_id", "feishu_id", "slack_id", "created_by", "updated_by", "comment", # 通用字段 ] fields_date = [ @@ -143,11 +143,11 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer # 外键的字段 fields_fk = [] # 多对多字段 - fields_m2m = ["groups", "system_roles", "org_roles", ] + fields_m2m = ["groups", "system_roles", "org_roles", "labels"] # 在serializer 上定义的字段 fields_custom = ["login_blocked", "password_strategy"] fields = fields_verbose + fields_fk + fields_m2m + fields_custom - fields_unexport = ["avatar_url", ] + fields_unexport = ["avatar_url", "is_service_account"] read_only_fields = [ "date_joined", "last_login", "created_by", @@ -259,6 +259,11 @@ class UserSerializer(RolesSerializerMixin, CommonBulkSerializerMixin, serializer ) return instance + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related('groups', 'labels', 'labels__label') + return queryset + class UserRetrieveSerializer(UserSerializer): login_confirm_settings = serializers.PrimaryKeyRelatedField( diff --git a/apps/users/tasks.py b/apps/users/tasks.py index ecc59c3bc..638e3bdec 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -86,19 +86,28 @@ 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(): + return + + uncommon_users_ttl = int(uncommon_users_ttl) + if uncommon_users_ttl <= 0 or uncommon_users_ttl >= 999: + return + seconds_to_subtract = uncommon_users_ttl * 24 * 60 * 60 t = timezone.now() - timedelta(seconds=seconds_to_subtract) - last_login_q = Q(last_login__lte=t) | Q(last_login__isnull=True) - api_key_q = Q(date_api_key_last_used__lte=t) | Q(date_api_key_last_used__isnull=True) + last_login_q = Q(last_login__lte=t) | (Q(last_login__isnull=True) & Q(date_joined__lte=t)) + api_key_q = Q(date_api_key_last_used__lte=t) | (Q(date_api_key_last_used__isnull=True) & Q(date_joined__lte=t)) users = User.objects \ .filter(date_joined__lt=t) \ .filter(is_active=True) \ .filter(last_login_q) \ - .filter(api_key_q) + .filter(api_key_q) \ + .exclude(username='admin') if not users: return + print("Some users are not used for a long time, and they will be disabled.") resource_ids = [] for user in users: diff --git a/apps/users/utils.py b/apps/users/utils.py index 29ce56cd1..9ddb5509d 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -11,7 +11,7 @@ from django.conf import settings from django.core.cache import cache from common.tasks import send_mail_async -from common.utils import reverse, get_object_or_none, ip +from common.utils import reverse, get_object_or_none, ip, safe_next_url from .models import User logger = logging.getLogger('jumpserver.users') @@ -49,6 +49,7 @@ def redirect_user_first_login_or_index(request, redirect_field_name): url = request.POST.get(redirect_field_name) if not url: url = request.GET.get(redirect_field_name) + url = safe_next_url(url, request=request) # 防止 next 地址为 None if not url or url.lower() in ['none']: url = reverse('index') @@ -94,7 +95,7 @@ def check_password_rules(password, is_org_admin=False): if settings.SECURITY_PASSWORD_NUMBER: pattern += '(?=.*\d)' if settings.SECURITY_PASSWORD_SPECIAL_CHAR: - pattern += '(?=.*[`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?])' + pattern += '(?=.*[`~!@#$%^&*()\-=_+\[\]{}|;:\'",.<>/?])' pattern += '[a-zA-Z\d`~!@#\$%\^&\*\(\)-=_\+\[\]\{\}\|;:\'\",\.<>\/\?]' if is_org_admin: min_length = settings.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH diff --git a/apps/users/views/profile/password.py b/apps/users/views/profile/password.py index 87d0bf6e7..8f345db93 100644 --- a/apps/users/views/profile/password.py +++ b/apps/users/views/profile/password.py @@ -30,7 +30,7 @@ class UserVerifyPasswordView(AuthMixin, FormView): try: password = form.cleaned_data['password'] except errors.AuthFailedError as e: - form.add_error("password", _(f"Password invalid") + f'({e.msg})') + form.add_error("password", _("Password invalid") + f'({e.msg})') return self.form_invalid(form) user = authenticate(request=self.request, username=user.username, password=password) diff --git a/poetry.lock b/poetry.lock index 959dcb844..3bf68f9c9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -40,111 +40,98 @@ reference = "tsinghua" [[package]] name = "aiohttp" -version = "3.8.5" +version = "3.9.1" description = "Async http client/server framework (asyncio)" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a94159871304770da4dd371f4291b20cac04e8c94f11bdea1c3478e557fbe0d8"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13bf85afc99ce6f9ee3567b04501f18f9f8dbbb2ea11ed1a2e079670403a7c84"}, - {file = "aiohttp-3.8.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ce2ac5708501afc4847221a521f7e4b245abf5178cf5ddae9d5b3856ddb2f3a"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96943e5dcc37a6529d18766597c491798b7eb7a61d48878611298afc1fca946c"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ad5c3c4590bb3cc28b4382f031f3783f25ec223557124c68754a2231d989e2b"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c413c633d0512df4dc7fd2373ec06cc6a815b7b6d6c2f208ada7e9e93a5061d"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df72ac063b97837a80d80dec8d54c241af059cc9bb42c4de68bd5b61ceb37caa"}, - {file = "aiohttp-3.8.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c48c5c0271149cfe467c0ff8eb941279fd6e3f65c9a388c984e0e6cf57538e14"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:368a42363c4d70ab52c2c6420a57f190ed3dfaca6a1b19afda8165ee16416a82"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7607ec3ce4993464368505888af5beb446845a014bc676d349efec0e05085905"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0d21c684808288a98914e5aaf2a7c6a3179d4df11d249799c32d1808e79503b5"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:312fcfbacc7880a8da0ae8b6abc6cc7d752e9caa0051a53d217a650b25e9a691"}, - {file = "aiohttp-3.8.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ad093e823df03bb3fd37e7dec9d4670c34f9e24aeace76808fc20a507cace825"}, - {file = "aiohttp-3.8.5-cp310-cp310-win32.whl", hash = "sha256:33279701c04351a2914e1100b62b2a7fdb9a25995c4a104259f9a5ead7ed4802"}, - {file = "aiohttp-3.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:6e4a280e4b975a2e7745573e3fc9c9ba0d1194a3738ce1cbaa80626cc9b4f4df"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ae871a964e1987a943d83d6709d20ec6103ca1eaf52f7e0d36ee1b5bebb8b9b9"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:461908b2578955045efde733719d62f2b649c404189a09a632d245b445c9c975"}, - {file = "aiohttp-3.8.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72a860c215e26192379f57cae5ab12b168b75db8271f111019509a1196dfc780"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc14be025665dba6202b6a71cfcdb53210cc498e50068bc088076624471f8bb9"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8af740fc2711ad85f1a5c034a435782fbd5b5f8314c9a3ef071424a8158d7f6b"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:841cd8233cbd2111a0ef0a522ce016357c5e3aff8a8ce92bcfa14cef890d698f"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed1c46fb119f1b59304b5ec89f834f07124cd23ae5b74288e364477641060ff"}, - {file = "aiohttp-3.8.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84f8ae3e09a34f35c18fa57f015cc394bd1389bce02503fb30c394d04ee6b938"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62360cb771707cb70a6fd114b9871d20d7dd2163a0feafe43fd115cfe4fe845e"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:23fb25a9f0a1ca1f24c0a371523546366bb642397c94ab45ad3aedf2941cec6a"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0ba0d15164eae3d878260d4c4df859bbdc6466e9e6689c344a13334f988bb53"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5d20003b635fc6ae3f96d7260281dfaf1894fc3aa24d1888a9b2628e97c241e5"}, - {file = "aiohttp-3.8.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0175d745d9e85c40dcc51c8f88c74bfbaef9e7afeeeb9d03c37977270303064c"}, - {file = "aiohttp-3.8.5-cp311-cp311-win32.whl", hash = "sha256:2e1b1e51b0774408f091d268648e3d57f7260c1682e7d3a63cb00d22d71bb945"}, - {file = "aiohttp-3.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:043d2299f6dfdc92f0ac5e995dfc56668e1587cea7f9aa9d8a78a1b6554e5755"}, - {file = "aiohttp-3.8.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cae533195e8122584ec87531d6df000ad07737eaa3c81209e85c928854d2195c"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f21e83f355643c345177a5d1d8079f9f28b5133bcd154193b799d380331d5d3"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a75ef35f2df54ad55dbf4b73fe1da96f370e51b10c91f08b19603c64004acc"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e2e9839e14dd5308ee773c97115f1e0a1cb1d75cbeeee9f33824fa5144c7634"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44e65da1de4403d0576473e2344828ef9c4c6244d65cf4b75549bb46d40b8dd"}, - {file = "aiohttp-3.8.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d847e4cde6ecc19125ccbc9bfac4a7ab37c234dd88fbb3c5c524e8e14da543"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:c7a815258e5895d8900aec4454f38dca9aed71085f227537208057853f9d13f2"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:8b929b9bd7cd7c3939f8bcfffa92fae7480bd1aa425279d51a89327d600c704d"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:5db3a5b833764280ed7618393832e0853e40f3d3e9aa128ac0ba0f8278d08649"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a0215ce6041d501f3155dc219712bc41252d0ab76474615b9700d63d4d9292af"}, - {file = "aiohttp-3.8.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:fd1ed388ea7fbed22c4968dd64bab0198de60750a25fe8c0c9d4bef5abe13824"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win32.whl", hash = "sha256:6e6783bcc45f397fdebc118d772103d751b54cddf5b60fbcc958382d7dd64f3e"}, - {file = "aiohttp-3.8.5-cp36-cp36m-win_amd64.whl", hash = "sha256:b5411d82cddd212644cf9360879eb5080f0d5f7d809d03262c50dad02f01421a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:01d4c0c874aa4ddfb8098e85d10b5e875a70adc63db91f1ae65a4b04d3344cda"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5980a746d547a6ba173fd5ee85ce9077e72d118758db05d229044b469d9029a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a482e6da906d5e6e653be079b29bc173a48e381600161c9932d89dfae5942ef"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80bd372b8d0715c66c974cf57fe363621a02f359f1ec81cba97366948c7fc873"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1161b345c0a444ebcf46bf0a740ba5dcf50612fd3d0528883fdc0eff578006a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd56db019015b6acfaaf92e1ac40eb8434847d9bf88b4be4efe5bfd260aee692"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:153c2549f6c004d2754cc60603d4668899c9895b8a89397444a9c4efa282aaf4"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4a01951fabc4ce26ab791da5f3f24dca6d9a6f24121746eb19756416ff2d881b"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bfb9162dcf01f615462b995a516ba03e769de0789de1cadc0f916265c257e5d8"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:7dde0009408969a43b04c16cbbe252c4f5ef4574ac226bc8815cd7342d2028b6"}, - {file = "aiohttp-3.8.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4149d34c32f9638f38f544b3977a4c24052042affa895352d3636fa8bffd030a"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win32.whl", hash = "sha256:68c5a82c8779bdfc6367c967a4a1b2aa52cd3595388bf5961a62158ee8a59e22"}, - {file = "aiohttp-3.8.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2cf57fb50be5f52bda004b8893e63b48530ed9f0d6c96c84620dc92fe3cd9b9d"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:eca4bf3734c541dc4f374ad6010a68ff6c6748f00451707f39857f429ca36ced"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1274477e4c71ce8cfe6c1ec2f806d57c015ebf84d83373676036e256bc55d690"}, - {file = "aiohttp-3.8.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:28c543e54710d6158fc6f439296c7865b29e0b616629767e685a7185fab4a6b9"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:910bec0c49637d213f5d9877105d26e0c4a4de2f8b1b29405ff37e9fc0ad52b8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5443910d662db951b2e58eb70b0fbe6b6e2ae613477129a5805d0b66c54b6cb7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e460be6978fc24e3df83193dc0cc4de46c9909ed92dd47d349a452ef49325b7"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1558def481d84f03b45888473fc5a1f35747b5f334ef4e7a571bc0dfcb11f8"}, - {file = "aiohttp-3.8.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34dd0c107799dcbbf7d48b53be761a013c0adf5571bf50c4ecad5643fe9cfcd0"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aa1990247f02a54185dc0dff92a6904521172a22664c863a03ff64c42f9b5410"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:0e584a10f204a617d71d359fe383406305a4b595b333721fa50b867b4a0a1548"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:a3cf433f127efa43fee6b90ea4c6edf6c4a17109d1d037d1a52abec84d8f2e42"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c11f5b099adafb18e65c2c997d57108b5bbeaa9eeee64a84302c0978b1ec948b"}, - {file = "aiohttp-3.8.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:84de26ddf621d7ac4c975dbea4c945860e08cccde492269db4e1538a6a6f3c35"}, - {file = "aiohttp-3.8.5-cp38-cp38-win32.whl", hash = "sha256:ab88bafedc57dd0aab55fa728ea10c1911f7e4d8b43e1d838a1739f33712921c"}, - {file = "aiohttp-3.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:5798a9aad1879f626589f3df0f8b79b3608a92e9beab10e5fda02c8a2c60db2e"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a6ce61195c6a19c785df04e71a4537e29eaa2c50fe745b732aa937c0c77169f3"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:773dd01706d4db536335fcfae6ea2440a70ceb03dd3e7378f3e815b03c97ab51"}, - {file = "aiohttp-3.8.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f83a552443a526ea38d064588613aca983d0ee0038801bc93c0c916428310c28"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f7372f7341fcc16f57b2caded43e81ddd18df53320b6f9f042acad41f8e049a"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea353162f249c8097ea63c2169dd1aa55de1e8fecbe63412a9bc50816e87b761"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d47ae48db0b2dcf70bc8a3bc72b3de86e2a590fc299fdbbb15af320d2659de"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d827176898a2b0b09694fbd1088c7a31836d1a505c243811c87ae53a3f6273c1"}, - {file = "aiohttp-3.8.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3562b06567c06439d8b447037bb655ef69786c590b1de86c7ab81efe1c9c15d8"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4e874cbf8caf8959d2adf572a78bba17cb0e9d7e51bb83d86a3697b686a0ab4d"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6809a00deaf3810e38c628e9a33271892f815b853605a936e2e9e5129762356c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:33776e945d89b29251b33a7e7d006ce86447b2cfd66db5e5ded4e5cd0340585c"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:eaeed7abfb5d64c539e2db173f63631455f1196c37d9d8d873fc316470dfbacd"}, - {file = "aiohttp-3.8.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e91d635961bec2d8f19dfeb41a539eb94bd073f075ca6dae6c8dc0ee89ad6f91"}, - {file = "aiohttp-3.8.5-cp39-cp39-win32.whl", hash = "sha256:00ad4b6f185ec67f3e6562e8a1d2b69660be43070bd0ef6fcec5211154c7df67"}, - {file = "aiohttp-3.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:c0a9034379a37ae42dea7ac1e048352d96286626251862e448933c0f59cbd79c"}, - {file = "aiohttp-3.8.5.tar.gz", hash = "sha256:b9552ec52cc147dbf1944ac7ac98af7602e51ea2dcd076ed194ca3c0d1c7d0bc"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0"}, + {file = "aiohttp-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f"}, + {file = "aiohttp-3.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d"}, + {file = "aiohttp-3.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501"}, + {file = "aiohttp-3.9.1-cp310-cp310-win32.whl", hash = "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489"}, + {file = "aiohttp-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e"}, + {file = "aiohttp-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d"}, + {file = "aiohttp-3.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd"}, + {file = "aiohttp-3.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a"}, + {file = "aiohttp-3.9.1-cp311-cp311-win32.whl", hash = "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544"}, + {file = "aiohttp-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821"}, + {file = "aiohttp-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c"}, + {file = "aiohttp-3.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f"}, + {file = "aiohttp-3.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f"}, + {file = "aiohttp-3.9.1-cp312-cp312-win32.whl", hash = "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed"}, + {file = "aiohttp-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672"}, + {file = "aiohttp-3.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361"}, + {file = "aiohttp-3.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a"}, + {file = "aiohttp-3.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8"}, + {file = "aiohttp-3.9.1-cp38-cp38-win32.whl", hash = "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4"}, + {file = "aiohttp-3.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0"}, + {file = "aiohttp-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f"}, + {file = "aiohttp-3.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f"}, + {file = "aiohttp-3.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c"}, + {file = "aiohttp-3.9.1-cp39-cp39-win32.whl", hash = "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7"}, + {file = "aiohttp-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf"}, + {file = "aiohttp-3.9.1.tar.gz", hash = "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d"}, ] [package.dependencies] aiosignal = ">=1.1.2" -async-timeout = ">=4.0.0a3,<5.0" attrs = ">=17.3.0" -charset-normalizer = ">=2.0,<4.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["Brotli", "aiodns", "cchardet"] +speedups = ["Brotli", "aiodns", "brotlicffi"] [package.source] type = "legacy" @@ -248,12 +235,12 @@ reference = "tsinghua" [[package]] name = "alibabacloud-openapi-util" -version = "0.2.1" +version = "0.2.2" description = "Aliyun Tea OpenApi Library for Python" optional = false python-versions = "*" files = [ - {file = "alibabacloud_openapi_util-0.2.1.tar.gz", hash = "sha256:5de2158a6e894b3364d9627090afdb6de4f6b92d1adf3a00e2c69206df30de15"}, + {file = "alibabacloud_openapi_util-0.2.2.tar.gz", hash = "sha256:ebbc3906f554cb4bf8f513e43e8a33e8b6a3d4a0ef13617a0e14c3dda8ef52a8"}, ] [package.dependencies] @@ -287,19 +274,19 @@ reference = "tsinghua" [[package]] name = "alibabacloud-tea-openapi" -version = "0.3.7" +version = "0.3.8" description = "Alibaba Cloud openapi SDK Library for Python" optional = false python-versions = ">=3.6" files = [ - {file = "alibabacloud_tea_openapi-0.3.7.tar.gz", hash = "sha256:bf677f3ddd35eeb499f08c0a38a024379c47dbcc0ba7a777a2ef7562abbe9c9f"}, + {file = "alibabacloud_tea_openapi-0.3.8.tar.gz", hash = "sha256:99796fa807433ee31e4e06239901d5dbc9cd22884f50f9bbb09b1dae9dbbb723"}, ] [package.dependencies] alibabacloud_credentials = ">=0.3.1,<1.0.0" alibabacloud_gateway_spi = ">=0.0.1,<1.0.0" alibabacloud_openapi_util = ">=0.2.1,<1.0.0" -alibabacloud_tea_util = ">=0.3.8,<1.0.0" +alibabacloud_tea_util = ">=0.3.11,<1.0.0" alibabacloud_tea_xml = ">=0.0.2,<1.0.0" [package.source] @@ -345,12 +332,12 @@ reference = "tsinghua" [[package]] name = "aliyun-python-sdk-core" -version = "2.13.36" +version = "2.14.0" description = "The core module of Aliyun Python SDK." optional = false python-versions = "*" files = [ - {file = "aliyun-python-sdk-core-2.13.36.tar.gz", hash = "sha256:20bd54984fa316da700c7f355a51ab0b816690e2a0fcefb7b5ef013fed0da928"}, + {file = "aliyun-python-sdk-core-2.14.0.tar.gz", hash = "sha256:c806815a48ffdb894cc5bce15b8259b9a3012cc0cda01be2f3dfbb844f3f4f21"}, ] [package.dependencies] @@ -402,13 +389,13 @@ reference = "tsinghua" [[package]] name = "aliyun-python-sdk-kms" -version = "2.16.1" +version = "2.16.2" description = "The kms module of Aliyun Python sdk." optional = false python-versions = "*" files = [ - {file = "aliyun-python-sdk-kms-2.16.1.tar.gz", hash = "sha256:a372737715682014bace68bd40fe83332f4fd925009a3eb110d41bc66f270e7a"}, - {file = "aliyun_python_sdk_kms-2.16.1-py2.py3-none-any.whl", hash = "sha256:9bc39c693ba83944f5dfb871b118a2925eb8a5ee214dfcce61ee2ea3b6317ef1"}, + {file = "aliyun-python-sdk-kms-2.16.2.tar.gz", hash = "sha256:f87234a8b64d457ca2338f87650db18a3ce7f7dbc9bfef71efe8f2894aded3d6"}, + {file = "aliyun_python_sdk_kms-2.16.2-py2.py3-none-any.whl", hash = "sha256:83166468817a4fbc4c958af43ec22856e1bd80f1363f56acf822206febe6b059"}, ] [package.dependencies] @@ -438,6 +425,22 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "ansible" version = "7.1.0" @@ -501,6 +504,31 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "anyio" +version = "3.7.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.7" +files = [ + {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, + {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] +test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (<0.22)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "appnope" version = "0.1.3" @@ -554,20 +582,21 @@ reference = "tsinghua" [[package]] name = "asttokens" -version = "2.2.1" +version = "2.4.1" description = "Annotate AST trees with source code positions" optional = false python-versions = "*" files = [ - {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, - {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, + {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, + {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, ] [package.dependencies] -six = "*" +six = ">=1.12.0" [package.extras] -test = ["astroid", "pytest"] +astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] +test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] [package.source] type = "legacy" @@ -687,13 +716,13 @@ reference = "tsinghua" [[package]] name = "azure-core" -version = "1.29.2" +version = "1.29.5" description = "Microsoft Azure Core Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "azure-core-1.29.2.zip", hash = "sha256:beb0fe88d1043d8457318e8fb841d9caa648211092eda213c16b376401f3710d"}, - {file = "azure_core-1.29.2-py3-none-any.whl", hash = "sha256:8e6602f322dc1070caf7e17754beb53b69ffa09df0f4786009a3107e9a00c793"}, + {file = "azure-core-1.29.5.tar.gz", hash = "sha256:52983c89d394c6f881a121e5101c5fa67278ca3b1f339c8fb2ef39230c70e9ac"}, + {file = "azure_core-1.29.5-py3-none-any.whl", hash = "sha256:0fa04b7b1f7d44a4fb8468c4093deb2ea01fdf4faddbf802ed9205615f99d68c"}, ] [package.dependencies] @@ -1020,13 +1049,13 @@ reference = "tsinghua" [[package]] name = "cachetools" -version = "5.3.1" +version = "5.3.2" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"}, - {file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"}, + {file = "cachetools-5.3.2-py3-none-any.whl", hash = "sha256:861f35a13a451f94e301ce2bec7cac63e881232ccce7ed67fab9b5df4d3beaa1"}, + {file = "cachetools-5.3.2.tar.gz", hash = "sha256:086ee420196f7b2ab9ca2db2520aca326318b68fe5ba8bc4d49cca91add450f2"}, ] [package.source] @@ -1259,86 +1288,101 @@ reference = "tsinghua" [[package]] name = "charset-normalizer" -version = "3.2.0" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.2.0.tar.gz", hash = "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win32.whl", hash = "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96"}, - {file = "charset_normalizer-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win32.whl", hash = "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1"}, - {file = "charset_normalizer-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win32.whl", hash = "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1"}, - {file = "charset_normalizer-3.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win32.whl", hash = "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706"}, - {file = "charset_normalizer-3.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win32.whl", hash = "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9"}, - {file = "charset_normalizer-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80"}, - {file = "charset_normalizer-3.2.0-py3-none-any.whl", hash = "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6"}, + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [package.source] @@ -1467,13 +1511,13 @@ reference = "tsinghua" [[package]] name = "constantly" -version = "15.1.0" +version = "23.10.4" description = "Symbolic constants in Python" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"}, - {file = "constantly-15.1.0.tar.gz", hash = "sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35"}, + {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"}, + {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"}, ] [package.source] @@ -1752,6 +1796,22 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "distro" +version = "1.8.0" +description = "Distro - an OS platform information API" +optional = false +python-versions = ">=3.6" +files = [ + {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, + {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "django" version = "4.1.10" @@ -1820,7 +1880,7 @@ reference = "tsinghua" [[package]] name = "django-cas-ng" version = "4.3.0" -description = "" +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+" optional = false python-versions = ">=3.7" files = [ @@ -1859,6 +1919,25 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "django-cors-headers" +version = "4.3.0" +description = "django-cors-headers is a Django application for handling the server headers required for Cross-Origin Resource Sharing (CORS)." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_cors_headers-4.3.0-py3-none-any.whl", hash = "sha256:bd36c7aea0d070e462f3383f0dc9ef717e5fdc2b10a99c98c285f16da84ffba2"}, + {file = "django_cors_headers-4.3.0.tar.gz", hash = "sha256:25aabc94d4837678c1edf442c7f68a5f5fd151f6767b0e0b01c61a2179d02711"}, +] + +[package.dependencies] +Django = ">=3.2" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "django-debug-toolbar" version = "4.1.0" @@ -2435,17 +2514,17 @@ reference = "tsinghua" [[package]] name = "executing" -version = "1.2.0" +version = "2.0.1" description = "Get the currently executing AST node of a frame, and other information" optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, - {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, ] [package.extras] -tests = ["asttokens", "littleutils", "pytest", "rich"] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] [package.source] type = "legacy" @@ -2643,13 +2722,13 @@ reference = "tsinghua" [[package]] name = "google-api-core" -version = "2.11.1" +version = "2.14.0" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.11.1.tar.gz", hash = "sha256:25d29e05a0058ed5f19c61c0a78b1b53adea4d9364b464d014fbda941f6d1c9a"}, - {file = "google_api_core-2.11.1-py3-none-any.whl", hash = "sha256:d92a5a92dc36dd4f4b9ee4e55528a90e432b059f93aee6ad857f9de8cc7ae94a"}, + {file = "google-api-core-2.14.0.tar.gz", hash = "sha256:5368a4502b793d9bbf812a5912e13e4e69f9bd87f6efb508460c43f5bbd1ce41"}, + {file = "google_api_core-2.14.0-py3-none-any.whl", hash = "sha256:de2fb50ed34d47ddbb2bd2dcf680ee8fead46279f4ed6b16de362aca23a18952"}, ] [package.dependencies] @@ -2678,21 +2757,19 @@ reference = "tsinghua" [[package]] name = "google-auth" -version = "2.22.0" +version = "2.23.4" description = "Google Authentication Library" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "google-auth-2.22.0.tar.gz", hash = "sha256:164cba9af4e6e4e40c3a4f90a1a6c12ee56f14c0b4868d1ca91b32826ab334ce"}, - {file = "google_auth-2.22.0-py2.py3-none-any.whl", hash = "sha256:d61d1b40897407b574da67da1a833bdc10d5a11642566e506565d1b1a46ba873"}, + {file = "google-auth-2.23.4.tar.gz", hash = "sha256:79905d6b1652187def79d491d6e23d0cbb3a21d3c7ba0dbaa9c8a01906b13ff3"}, + {file = "google_auth-2.23.4-py2.py3-none-any.whl", hash = "sha256:d4bbc92fe4b8bfd2f3e8d88e5ba7085935da208ee38a134fc280e7ce682a05f2"}, ] [package.dependencies] cachetools = ">=2.0.0,<6.0" pyasn1-modules = ">=0.2.1" rsa = ">=3.1.4,<5" -six = ">=1.9.0" -urllib3 = "<2.0" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] @@ -2729,13 +2806,13 @@ reference = "tsinghua" [[package]] name = "googleapis-common-protos" -version = "1.60.0" +version = "1.61.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.60.0.tar.gz", hash = "sha256:e73ebb404098db405ba95d1e1ae0aa91c3e15a71da031a2eeb6b2e23e7bc3708"}, - {file = "googleapis_common_protos-1.60.0-py2.py3-none-any.whl", hash = "sha256:69f9bbcc6acde92cab2db95ce30a70bd2b81d20b12eff3f1aabaffcbe8a93918"}, + {file = "googleapis-common-protos-1.61.0.tar.gz", hash = "sha256:8a64866a97f6304a7179873a465d6eee97b7a24ec6cfd78e0f575e96b821240b"}, + {file = "googleapis_common_protos-1.61.0-py2.py3-none-any.whl", hash = "sha256:22f1915393bb3245343f6efe87f6fe868532efc12aa26b391b15132e1279f1c0"}, ] [package.dependencies] @@ -2751,75 +2828,72 @@ reference = "tsinghua" [[package]] name = "greenlet" -version = "2.0.2" +version = "3.0.1" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" files = [ - {file = "greenlet-2.0.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:bdfea8c661e80d3c1c99ad7c3ff74e6e87184895bbaca6ee8cc61209f8b9b85d"}, - {file = "greenlet-2.0.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9d14b83fab60d5e8abe587d51c75b252bcc21683f24699ada8fb275d7712f5a9"}, - {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, - {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, - {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, - {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d75209eed723105f9596807495d58d10b3470fa6732dd6756595e89925ce2470"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3a51c9751078733d88e013587b108f1b7a1fb106d402fb390740f002b6f6551a"}, - {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, - {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, - {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eff4eb9b7eb3e4d0cae3d28c283dc16d9bed6b193c2e1ace3ed86ce48ea8df19"}, - {file = "greenlet-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5454276c07d27a740c5892f4907c86327b632127dd9abec42ee62e12427ff7e3"}, - {file = "greenlet-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:7cafd1208fdbe93b67c7086876f061f660cfddc44f404279c1585bbf3cdc64c5"}, - {file = "greenlet-2.0.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:910841381caba4f744a44bf81bfd573c94e10b3045ee00de0cbf436fe50673a6"}, - {file = "greenlet-2.0.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:18a7f18b82b52ee85322d7a7874e676f34ab319b9f8cce5de06067384aa8ff43"}, - {file = "greenlet-2.0.2-cp35-cp35m-win32.whl", hash = "sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a"}, - {file = "greenlet-2.0.2-cp35-cp35m-win_amd64.whl", hash = "sha256:4b58adb399c4d61d912c4c331984d60eb66565175cdf4a34792cd9600f21b394"}, - {file = "greenlet-2.0.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:703f18f3fda276b9a916f0934d2fb6d989bf0b4fb5a64825260eb9bfd52d78f0"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:32e5b64b148966d9cccc2c8d35a671409e45f195864560829f395a54226408d3"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd11f291565a81d71dab10b7033395b7a3a5456e637cf997a6f33ebdf06f8db"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0f72c9ddb8cd28532185f54cc1453f2c16fb417a08b53a855c4e6a418edd099"}, - {file = "greenlet-2.0.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd021c754b162c0fb55ad5d6b9d960db667faad0fa2ff25bb6e1301b0b6e6a75"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3c9b12575734155d0c09d6c3e10dbd81665d5c18e1a7c6597df72fd05990c8cf"}, - {file = "greenlet-2.0.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b9ec052b06a0524f0e35bd8790686a1da006bd911dd1ef7d50b77bfbad74e292"}, - {file = "greenlet-2.0.2-cp36-cp36m-win32.whl", hash = "sha256:dbfcfc0218093a19c252ca8eb9aee3d29cfdcb586df21049b9d777fd32c14fd9"}, - {file = "greenlet-2.0.2-cp36-cp36m-win_amd64.whl", hash = "sha256:9f35ec95538f50292f6d8f2c9c9f8a3c6540bbfec21c9e5b4b751e0a7c20864f"}, - {file = "greenlet-2.0.2-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:d5508f0b173e6aa47273bdc0a0b5ba055b59662ba7c7ee5119528f466585526b"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:f82d4d717d8ef19188687aa32b8363e96062911e63ba22a0cff7802a8e58e5f1"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9c59a2120b55788e800d82dfa99b9e156ff8f2227f07c5e3012a45a399620b7"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2780572ec463d44c1d3ae850239508dbeb9fed38e294c68d19a24d925d9223ca"}, - {file = "greenlet-2.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937e9020b514ceedb9c830c55d5c9872abc90f4b5862f89c0887033ae33c6f73"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:36abbf031e1c0f79dd5d596bfaf8e921c41df2bdf54ee1eed921ce1f52999a86"}, - {file = "greenlet-2.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:18e98fb3de7dba1c0a852731c3070cf022d14f0d68b4c87a19cc1016f3bb8b33"}, - {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, - {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, - {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acd2162a36d3de67ee896c43effcd5ee3de247eb00354db411feb025aa319857"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a"}, - {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, - {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, - {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be4ed120b52ae4d974aa40215fcdfde9194d63541c7ded40ee12eb4dda57b76b"}, - {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c817e84245513926588caf1152e3b559ff794d505555211ca041f032abbb6b"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1a819eef4b0e0b96bb0d98d797bef17dc1b4a10e8d7446be32d1da33e095dbb8"}, - {file = "greenlet-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7efde645ca1cc441d6dc4b48c0f7101e8d86b54c8530141b09fd31cef5149ec9"}, - {file = "greenlet-2.0.2-cp39-cp39-win32.whl", hash = "sha256:ea9872c80c132f4663822dd2a08d404073a5a9b5ba6155bea72fb2a79d1093b5"}, - {file = "greenlet-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:db1a39669102a1d8d12b57de2bb7e2ec9066a6f2b3da35ae511ff93b01b5d564"}, - {file = "greenlet-2.0.2.tar.gz", hash = "sha256:e7c8dc13af7db097bed64a051d2dd49e9f0af495c26995c00a9ee842690d34c0"}, + {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, + {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, + {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, + {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, + {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, + {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, + {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, + {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, + {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, + {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, + {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, + {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, + {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, + {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, + {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, + {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, + {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, + {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, + {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, + {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, + {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, + {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, + {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, + {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, + {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, + {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, + {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, ] [package.extras] -docs = ["Sphinx", "docutils (<0.18)"] +docs = ["Sphinx"] test = ["objgraph", "psutil"] [package.source] @@ -2967,6 +3041,32 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "httpcore" +version = "1.0.2" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.23.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "httpsig" version = "1.3.0" @@ -2987,6 +3087,35 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "httpx" +version = "0.25.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, + {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "huaweicloudsdkcore" version = "3.1.52" @@ -3033,13 +3162,13 @@ reference = "tsinghua" [[package]] name = "humanize" -version = "4.8.0" +version = "4.9.0" description = "Python humanize utilities" optional = false python-versions = ">=3.8" files = [ - {file = "humanize-4.8.0-py3-none-any.whl", hash = "sha256:8bc9e2bb9315e61ec06bf690151ae35aeb65651ab091266941edf97c90836404"}, - {file = "humanize-4.8.0.tar.gz", hash = "sha256:9783373bf1eec713a770ecaa7c2d7a7902c98398009dfa3d8a2df91eec9311e8"}, + {file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"}, + {file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"}, ] [package.extras] @@ -3216,13 +3345,13 @@ reference = "tsinghua" [[package]] name = "iso8601" -version = "2.0.0" +version = "2.1.0" description = "Simple module to parse ISO 8601 dates" optional = false python-versions = ">=3.7,<4.0" files = [ - {file = "iso8601-2.0.0-py3-none-any.whl", hash = "sha256:ebe10061b932edb8a8e33cc635d661926c59b9c3bed7a4f4edca8c62d400af10"}, - {file = "iso8601-2.0.0.tar.gz", hash = "sha256:739960d37c74c77bd9bd546a76562ccb581fe3d4820ff5c3141eb49c839fda8f"}, + {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, + {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, ] [package.source] @@ -3283,13 +3412,13 @@ reference = "tsinghua" [[package]] name = "jedi" -version = "0.19.0" +version = "0.19.1" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, - {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, + {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, + {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, ] [package.dependencies] @@ -3298,7 +3427,7 @@ parso = ">=0.8.3,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [package.source] type = "legacy" @@ -3373,6 +3502,11 @@ requests = "2.31.0" s3transfer = "0.6.1" six = "1.16.0" +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "jsonfield2" version = "4.0.0.post0" @@ -3680,6 +3814,16 @@ 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"}, @@ -3738,12 +3882,78 @@ reference = "tsinghua" [[package]] name = "maxminddb" -version = "2.4.0" +version = "2.5.1" description = "Reader for the MaxMind DB format" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "maxminddb-2.4.0.tar.gz", hash = "sha256:81e54e53408bd502650e5969ccba16780af659ec1db1c44b2c997e4330a5ed96"}, + {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"}, +] + +[package.dependencies] +setuptools = ">=68.2.2" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +optional = false +python-versions = "*" +files = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, ] [package.source] @@ -3753,13 +3963,13 @@ reference = "tsinghua" [[package]] name = "msal" -version = "1.23.0" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +version = "1.25.0" +description = "The Microsoft Authentication Library (MSAL) for Python library" optional = false -python-versions = "*" +python-versions = ">=2.7" files = [ - {file = "msal-1.23.0-py2.py3-none-any.whl", hash = "sha256:3342e0837a047007f9d479e814b559c3219767453d57920dc40a31986862048b"}, - {file = "msal-1.23.0.tar.gz", hash = "sha256:25c9a33acf84301f93d1fdbe9f1a9c60cd38af0d5fffdbfa378138fc7bc1e86b"}, + {file = "msal-1.25.0-py2.py3-none-any.whl", hash = "sha256:386df621becb506bc315a713ec3d4d5b5d6163116955c7dde23622f156b81af6"}, + {file = "msal-1.25.0.tar.gz", hash = "sha256:f44329fdb59f4f044c779164a34474b8a44ad9e4940afbc4c3a3a2bbe90324d9"}, ] [package.dependencies] @@ -3800,74 +4010,67 @@ reference = "tsinghua" [[package]] name = "msgpack" -version = "1.0.5" +version = "1.0.7" description = "MessagePack serializer" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:525228efd79bb831cf6830a732e2e80bc1b05436b086d4264814b4b2955b2fa9"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4f8d8b3bf1ff2672567d6b5c725a1b347fe838b912772aa8ae2bf70338d5a198"}, - {file = "msgpack-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdc793c50be3f01106245a61b739328f7dccc2c648b501e237f0699fe1395b81"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cb47c21a8a65b165ce29f2bec852790cbc04936f502966768e4aae9fa763cb7"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42b9594cc3bf4d838d67d6ed62b9e59e201862a25e9a157019e171fbe672dd3"}, - {file = "msgpack-1.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:55b56a24893105dc52c1253649b60f475f36b3aa0fc66115bffafb624d7cb30b"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:1967f6129fc50a43bfe0951c35acbb729be89a55d849fab7686004da85103f1c"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20a97bf595a232c3ee6d57ddaadd5453d174a52594bf9c21d10407e2a2d9b3bd"}, - {file = "msgpack-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d25dd59bbbbb996eacf7be6b4ad082ed7eacc4e8f3d2df1ba43822da9bfa122a"}, - {file = "msgpack-1.0.5-cp310-cp310-win32.whl", hash = "sha256:382b2c77589331f2cb80b67cc058c00f225e19827dbc818d700f61513ab47bea"}, - {file = "msgpack-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:4867aa2df9e2a5fa5f76d7d5565d25ec76e84c106b55509e78c1ede0f152659a"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9f5ae84c5c8a857ec44dc180a8b0cc08238e021f57abdf51a8182e915e6299f0"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e6ca5d5699bcd89ae605c150aee83b5321f2115695e741b99618f4856c50898"}, - {file = "msgpack-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5494ea30d517a3576749cad32fa27f7585c65f5f38309c88c6d137877fa28a5a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ab2f3331cb1b54165976a9d976cb251a83183631c88076613c6c780f0d6e45a"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28592e20bbb1620848256ebc105fc420436af59515793ed27d5c77a217477705"}, - {file = "msgpack-1.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe5c63197c55bce6385d9aee16c4d0641684628f63ace85f73571e65ad1c1e8d"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed40e926fa2f297e8a653c954b732f125ef97bdd4c889f243182299de27e2aa9"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b2de4c1c0538dcb7010902a2b97f4e00fc4ddf2c8cda9749af0e594d3b7fa3d7"}, - {file = "msgpack-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf22a83f973b50f9d38e55c6aade04c41ddda19b00c4ebc558930d78eecc64ed"}, - {file = "msgpack-1.0.5-cp311-cp311-win32.whl", hash = "sha256:c396e2cc213d12ce017b686e0f53497f94f8ba2b24799c25d913d46c08ec422c"}, - {file = "msgpack-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c4c68d87497f66f96d50142a2b73b97972130d93677ce930718f68828b382e2"}, - {file = "msgpack-1.0.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a2b031c2e9b9af485d5e3c4520f4220d74f4d222a5b8dc8c1a3ab9448ca79c57"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f837b93669ce4336e24d08286c38761132bc7ab29782727f8557e1eb21b2080"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1d46dfe3832660f53b13b925d4e0fa1432b00f5f7210eb3ad3bb9a13c6204a6"}, - {file = "msgpack-1.0.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:366c9a7b9057e1547f4ad51d8facad8b406bab69c7d72c0eb6f529cf76d4b85f"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4c075728a1095efd0634a7dccb06204919a2f67d1893b6aa8e00497258bf926c"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:f933bbda5a3ee63b8834179096923b094b76f0c7a73c1cfe8f07ad608c58844b"}, - {file = "msgpack-1.0.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:36961b0568c36027c76e2ae3ca1132e35123dcec0706c4b7992683cc26c1320c"}, - {file = "msgpack-1.0.5-cp36-cp36m-win32.whl", hash = "sha256:b5ef2f015b95f912c2fcab19c36814963b5463f1fb9049846994b007962743e9"}, - {file = "msgpack-1.0.5-cp36-cp36m-win_amd64.whl", hash = "sha256:288e32b47e67f7b171f86b030e527e302c91bd3f40fd9033483f2cacc37f327a"}, - {file = "msgpack-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:137850656634abddfb88236008339fdaba3178f4751b28f270d2ebe77a563b6c"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c05a4a96585525916b109bb85f8cb6511db1c6f5b9d9cbcbc940dc6b4be944b"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a62ec00b636583e5cb6ad313bbed36bb7ead5fa3a3e38938503142c72cba4f"}, - {file = "msgpack-1.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef8108f8dedf204bb7b42994abf93882da1159728a2d4c5e82012edd92c9da9f"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1835c84d65f46900920b3708f5ba829fb19b1096c1800ad60bae8418652a951d"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:e57916ef1bd0fee4f21c4600e9d1da352d8816b52a599c46460e93a6e9f17086"}, - {file = "msgpack-1.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:17358523b85973e5f242ad74aa4712b7ee560715562554aa2134d96e7aa4cbbf"}, - {file = "msgpack-1.0.5-cp37-cp37m-win32.whl", hash = "sha256:cb5aaa8c17760909ec6cb15e744c3ebc2ca8918e727216e79607b7bbce9c8f77"}, - {file = "msgpack-1.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:ab31e908d8424d55601ad7075e471b7d0140d4d3dd3272daf39c5c19d936bd82"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b72d0698f86e8d9ddf9442bdedec15b71df3598199ba33322d9711a19f08145c"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:379026812e49258016dd84ad79ac8446922234d498058ae1d415f04b522d5b2d"}, - {file = "msgpack-1.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:332360ff25469c346a1c5e47cbe2a725517919892eda5cfaffe6046656f0b7bb"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:476a8fe8fae289fdf273d6d2a6cb6e35b5a58541693e8f9f019bfe990a51e4ba"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9985b214f33311df47e274eb788a5893a761d025e2b92c723ba4c63936b69b1"}, - {file = "msgpack-1.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48296af57cdb1d885843afd73c4656be5c76c0c6328db3440c9601a98f303d87"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:addab7e2e1fcc04bd08e4eb631c2a90960c340e40dfc4a5e24d2ff0d5a3b3edb"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:916723458c25dfb77ff07f4c66aed34e47503b2eb3188b3adbec8d8aa6e00f48"}, - {file = "msgpack-1.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:821c7e677cc6acf0fd3f7ac664c98803827ae6de594a9f99563e48c5a2f27eb0"}, - {file = "msgpack-1.0.5-cp38-cp38-win32.whl", hash = "sha256:1c0f7c47f0087ffda62961d425e4407961a7ffd2aa004c81b9c07d9269512f6e"}, - {file = "msgpack-1.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:bae7de2026cbfe3782c8b78b0db9cbfc5455e079f1937cb0ab8d133496ac55e1"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:20c784e66b613c7f16f632e7b5e8a1651aa5702463d61394671ba07b2fc9e025"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:266fa4202c0eb94d26822d9bfd7af25d1e2c088927fe8de9033d929dd5ba24c5"}, - {file = "msgpack-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18334484eafc2b1aa47a6d42427da7fa8f2ab3d60b674120bce7a895a0a85bdd"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57e1f3528bd95cc44684beda696f74d3aaa8a5e58c816214b9046512240ef437"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:586d0d636f9a628ddc6a17bfd45aa5b5efaf1606d2b60fa5d87b8986326e933f"}, - {file = "msgpack-1.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a740fa0e4087a734455f0fc3abf5e746004c9da72fbd541e9b113013c8dc3282"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3055b0455e45810820db1f29d900bf39466df96ddca11dfa6d074fa47054376d"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a61215eac016f391129a013c9e46f3ab308db5f5ec9f25811e811f96962599a8"}, - {file = "msgpack-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:362d9655cd369b08fda06b6657a303eb7172d5279997abe094512e919cf74b11"}, - {file = "msgpack-1.0.5-cp39-cp39-win32.whl", hash = "sha256:ac9dd47af78cae935901a9a500104e2dea2e253207c924cc95de149606dc43cc"}, - {file = "msgpack-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:06f5174b5f8ed0ed919da0e62cbd4ffde676a374aba4020034da05fab67b9164"}, - {file = "msgpack-1.0.5.tar.gz", hash = "sha256:c075544284eadc5cddc70f4757331d99dcbc16b2bbd4849d15f8aae4cf36d31c"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329"}, + {file = "msgpack-1.0.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee"}, + {file = "msgpack-1.0.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1"}, + {file = "msgpack-1.0.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681"}, + {file = "msgpack-1.0.7-cp310-cp310-win32.whl", hash = "sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9"}, + {file = "msgpack-1.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93"}, + {file = "msgpack-1.0.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b"}, + {file = "msgpack-1.0.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c"}, + {file = "msgpack-1.0.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e"}, + {file = "msgpack-1.0.7-cp311-cp311-win32.whl", hash = "sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1"}, + {file = "msgpack-1.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4"}, + {file = "msgpack-1.0.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672"}, + {file = "msgpack-1.0.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c"}, + {file = "msgpack-1.0.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5"}, + {file = "msgpack-1.0.7-cp312-cp312-win32.whl", hash = "sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9"}, + {file = "msgpack-1.0.7-cp312-cp312-win_amd64.whl", hash = "sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0"}, + {file = "msgpack-1.0.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524"}, + {file = "msgpack-1.0.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf"}, + {file = "msgpack-1.0.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c"}, + {file = "msgpack-1.0.7-cp38-cp38-win32.whl", hash = "sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2"}, + {file = "msgpack-1.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81"}, + {file = "msgpack-1.0.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7"}, + {file = "msgpack-1.0.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd"}, + {file = "msgpack-1.0.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f"}, + {file = "msgpack-1.0.7-cp39-cp39-win32.whl", hash = "sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad"}, + {file = "msgpack-1.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3"}, + {file = "msgpack-1.0.7.tar.gz", hash = "sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87"}, ] [package.source] @@ -4033,13 +4236,13 @@ reference = "tsinghua" [[package]] name = "netaddr" -version = "0.8.0" +version = "0.9.0" description = "A network address manipulation library for Python" optional = false python-versions = "*" files = [ - {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"}, - {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"}, + {file = "netaddr-0.9.0-py3-none-any.whl", hash = "sha256:5148b1055679d2a1ec070c521b7db82137887fabd6d7e37f5199b44f775c3bb1"}, + {file = "netaddr-0.9.0.tar.gz", hash = "sha256:7b46fa9b1a2d71fd5de9e4a3784ef339700a53a08c8040f08baf5f1194da0128"}, ] [package.source] @@ -4127,6 +4330,34 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "openai" +version = "1.3.7" +description = "The official Python library for the openai API" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "openai-1.3.7-py3-none-any.whl", hash = "sha256:e5c51367a910297e4d1cd33d2298fb87d7edf681edbe012873925ac16f95bee0"}, + {file = "openai-1.3.7.tar.gz", hash = "sha256:18074a0f51f9b49d1ae268c7abc36f7f33212a0c0d08ce11b7053ab2d17798de"}, +] + +[package.dependencies] +anyio = ">=3.5.0,<4" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tqdm = ">4" +typing-extensions = ">=4.5,<5" + +[package.extras] +datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "openapi-codec" version = "1.3.2" @@ -4230,13 +4461,13 @@ reference = "tsinghua" [[package]] name = "oslo-config" -version = "9.1.1" +version = "9.2.0" description = "Oslo Configuration API" optional = false python-versions = ">=3.8" files = [ - {file = "oslo.config-9.1.1-py3-none-any.whl", hash = "sha256:7cd56e0b41b04f64dbc42e83e8164d5ef03466390f1216fbda2cb0e1c535c22c"}, - {file = "oslo.config-9.1.1.tar.gz", hash = "sha256:b07654b53d87792ae8e739962ad729c529c9938a118d891ece9ee31d59716bc9"}, + {file = "oslo.config-9.2.0-py3-none-any.whl", hash = "sha256:b98e50b19161fc76f25905ff74043e239258a3ebe799a5f9070d285e3c039dee"}, + {file = "oslo.config-9.2.0.tar.gz", hash = "sha256:ffeb01ca65a603d5525905f1a88a3319be09ce2c6ac376c4312aaec283095878"}, ] [package.dependencies] @@ -4250,7 +4481,7 @@ stevedore = ">=1.20.0" [package.extras] rst-generator = ["rst2txt (>=1.1.0)", "sphinx (>=1.8.0,!=2.1.0)"] -test = ["bandit (>=1.6.0,<1.7.0)", "coverage (>=4.0,!=4.4)", "fixtures (>=3.0.0)", "hacking (>=3.0.1,<3.1.0)", "mypy (>=0.720)", "oslo.log (>=3.36.0)", "oslotest (>=3.2.0)", "pre-commit (>=2.6.0)", "requests-mock (>=1.5.0)", "stestr (>=2.1.0)", "testscenarios (>=0.4)", "testtools (>=2.2.0)"] +test = ["bandit (>=1.7.0,<1.8.0)", "coverage (>=4.0,!=4.4)", "fixtures (>=3.0.0)", "hacking (>=3.0.1,<3.1.0)", "mypy (>=0.720)", "oslo.log (>=3.36.0)", "oslotest (>=3.2.0)", "pre-commit (>=2.6.0)", "requests-mock (>=1.5.0)", "stestr (>=2.1.0)", "testscenarios (>=0.4)", "testtools (>=2.2.0)"] [package.source] type = "legacy" @@ -4259,13 +4490,13 @@ reference = "tsinghua" [[package]] name = "oslo-i18n" -version = "6.0.0" +version = "6.2.0" description = "Oslo i18n library" optional = false python-versions = ">=3.8" files = [ - {file = "oslo.i18n-6.0.0-py3-none-any.whl", hash = "sha256:080fedf41b05d4dcd23a91d23ee2dea0863996e860a59695856269a42d939fc1"}, - {file = "oslo.i18n-6.0.0.tar.gz", hash = "sha256:ed10686b75f7c607825177a669155f4e259ce39f6143a375f6359bbcaa4a35cd"}, + {file = "oslo.i18n-6.2.0-py3-none-any.whl", hash = "sha256:5cd6d0659bec2013107d235a8cf5e61475cc9dd33ef9ffc7aa2776bc1c6b56c9"}, + {file = "oslo.i18n-6.2.0.tar.gz", hash = "sha256:70f8a4ce9871291bc609d07e31e6e5032666556992ff1ae53e78f2ed2a5abe82"}, ] [package.dependencies] @@ -4278,13 +4509,13 @@ reference = "tsinghua" [[package]] name = "oslo-serialization" -version = "5.1.1" +version = "5.2.0" description = "Oslo Serialization library" optional = false python-versions = ">=3.8" files = [ - {file = "oslo.serialization-5.1.1-py3-none-any.whl", hash = "sha256:c5dfb97ce8ddd1d2708a9a3f4a091063f6c304940c7cb39f532f7f791441fdca"}, - {file = "oslo.serialization-5.1.1.tar.gz", hash = "sha256:8abbda8b1763a06071fc28c5d8a9be547ba285f4830e68a70ff88fe11f16bf43"}, + {file = "oslo.serialization-5.2.0-py3-none-any.whl", hash = "sha256:c7ec759192a787c7e1a5e765920bb594752c75e6e0cd5a9a82c385a9088125e5"}, + {file = "oslo.serialization-5.2.0.tar.gz", hash = "sha256:9cf030d61a6cce1f47a62d4050f5e83e1bd1a1018ac671bb193aee07d15bdbc2"}, ] [package.dependencies] @@ -4292,6 +4523,7 @@ msgpack = ">=0.5.2" "oslo.utils" = ">=3.33.0" pbr = ">=2.0.0,<2.1.0 || >2.1.0" pytz = ">=2013.6" +tzdata = ">=2022.4" [package.source] type = "legacy" @@ -4300,13 +4532,13 @@ reference = "tsinghua" [[package]] name = "oslo-utils" -version = "6.2.0" +version = "6.3.0" description = "Oslo Utility library" optional = false python-versions = ">=3.8" files = [ - {file = "oslo.utils-6.2.0-py3-none-any.whl", hash = "sha256:30ba9fd431be468cd17b5d7c1a0ae6d63bb63aaaf97bf590123f13c6d95254a3"}, - {file = "oslo.utils-6.2.0.tar.gz", hash = "sha256:fe1d166f4cb004fbd6b6bc9adfbc32aedeaf3eb54eeaf70d91a224a87543c6a5"}, + {file = "oslo.utils-6.3.0-py3-none-any.whl", hash = "sha256:6bac2e56650f502caae6c0e8ba6e5eda3d7a16743d115f8836cad54538dd667f"}, + {file = "oslo.utils-6.3.0.tar.gz", hash = "sha256:758d945b2bad5bea81abed80ad33ffea1d1d793348ac5eb5b3866ba745b11d55"}, ] [package.dependencies] @@ -4318,6 +4550,7 @@ netifaces = ">=0.10.4" packaging = ">=20.4" pyparsing = ">=2.1.0" pytz = ">=2013.6" +PyYAML = ">=3.13" tzdata = ">=2022.4" [package.source] @@ -4350,13 +4583,13 @@ reference = "tsinghua" [[package]] name = "packaging" -version = "23.1" +version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, - {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [package.source] @@ -4434,13 +4667,13 @@ reference = "tsinghua" [[package]] name = "pbr" -version = "5.11.1" +version = "6.0.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, - {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, + {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, + {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, ] [package.source] @@ -4450,13 +4683,13 @@ reference = "tsinghua" [[package]] name = "pexpect" -version = "4.8.0" +version = "4.9.0" description = "Pexpect allows easy control of interactive console applications." optional = false python-versions = "*" files = [ - {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, - {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, ] [package.dependencies] @@ -4575,13 +4808,13 @@ reference = "tsinghua" [[package]] name = "portalocker" -version = "2.7.0" +version = "2.8.2" description = "Wraps the portalocker recipe for easy usage" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" files = [ - {file = "portalocker-2.7.0-py2.py3-none-any.whl", hash = "sha256:a07c5b4f3985c3cf4798369631fb7011adb498e2a46d8440efc75a8f29a0f983"}, - {file = "portalocker-2.7.0.tar.gz", hash = "sha256:032e81d534a88ec1736d03f780ba073f047a06c478b06e2937486f334e955c51"}, + {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, + {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, ] [package.dependencies] @@ -4590,7 +4823,7 @@ pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} [package.extras] docs = ["sphinx (>=1.7.1)"] redis = ["redis"] -tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] [package.source] type = "legacy" @@ -4599,13 +4832,13 @@ reference = "tsinghua" [[package]] name = "prettytable" -version = "3.8.0" +version = "3.9.0" description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" optional = false python-versions = ">=3.8" files = [ - {file = "prettytable-3.8.0-py3-none-any.whl", hash = "sha256:03481bca25ae0c28958c8cd6ac5165c159ce89f7ccde04d5c899b24b68bb13b7"}, - {file = "prettytable-3.8.0.tar.gz", hash = "sha256:031eae6a9102017e8c7c7906460d150b7ed78b20fd1d8c8be4edaf88556c07ce"}, + {file = "prettytable-3.9.0-py3-none-any.whl", hash = "sha256:a71292ab7769a5de274b146b276ce938786f56c31cf7cea88b6f3775d82fe8c8"}, + {file = "prettytable-3.9.0.tar.gz", hash = "sha256:f4ed94803c23073a90620b201965e5dc0bccf1760b7a7eaf3158cab8aaffdf34"}, ] [package.dependencies] @@ -4621,13 +4854,13 @@ reference = "tsinghua" [[package]] name = "prometheus-client" -version = "0.17.1" +version = "0.19.0" description = "Python client for the Prometheus monitoring system." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.17.1-py3-none-any.whl", hash = "sha256:e537f37160f6807b8202a6fc4764cdd19bac5480ddd3e0d463c3002b34462101"}, - {file = "prometheus_client-0.17.1.tar.gz", hash = "sha256:21e674f39831ae3f8acde238afd9a27a37d0d2fb5a28ea094f0ce25d2cbf2091"}, + {file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"}, + {file = "prometheus_client-0.19.0.tar.gz", hash = "sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1"}, ] [package.extras] @@ -4640,13 +4873,13 @@ reference = "tsinghua" [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.41" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, + {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, ] [package.dependencies] @@ -4681,24 +4914,22 @@ reference = "tsinghua" [[package]] name = "protobuf" -version = "4.24.0" +version = "4.25.1" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "protobuf-4.24.0-cp310-abi3-win32.whl", hash = "sha256:81cb9c4621d2abfe181154354f63af1c41b00a4882fb230b4425cbaed65e8f52"}, - {file = "protobuf-4.24.0-cp310-abi3-win_amd64.whl", hash = "sha256:6c817cf4a26334625a1904b38523d1b343ff8b637d75d2c8790189a4064e51c3"}, - {file = "protobuf-4.24.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ae97b5de10f25b7a443b40427033e545a32b0e9dda17bcd8330d70033379b3e5"}, - {file = "protobuf-4.24.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:567fe6b0647494845d0849e3d5b260bfdd75692bf452cdc9cb660d12457c055d"}, - {file = "protobuf-4.24.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:a6b1ca92ccabfd9903c0c7dde8876221dc7d8d87ad5c42e095cc11b15d3569c7"}, - {file = "protobuf-4.24.0-cp37-cp37m-win32.whl", hash = "sha256:a38400a692fd0c6944c3c58837d112f135eb1ed6cdad5ca6c5763336e74f1a04"}, - {file = "protobuf-4.24.0-cp37-cp37m-win_amd64.whl", hash = "sha256:5ab19ee50037d4b663c02218a811a5e1e7bb30940c79aac385b96e7a4f9daa61"}, - {file = "protobuf-4.24.0-cp38-cp38-win32.whl", hash = "sha256:e8834ef0b4c88666ebb7c7ec18045aa0f4325481d724daa624a4cf9f28134653"}, - {file = "protobuf-4.24.0-cp38-cp38-win_amd64.whl", hash = "sha256:8bb52a2be32db82ddc623aefcedfe1e0eb51da60e18fcc908fb8885c81d72109"}, - {file = "protobuf-4.24.0-cp39-cp39-win32.whl", hash = "sha256:ae7a1835721086013de193311df858bc12cd247abe4ef9710b715d930b95b33e"}, - {file = "protobuf-4.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:44825e963008f8ea0d26c51911c30d3e82e122997c3c4568fd0385dd7bacaedf"}, - {file = "protobuf-4.24.0-py3-none-any.whl", hash = "sha256:82e6e9ebdd15b8200e8423676eab38b774624d6a1ad696a60d86a2ac93f18201"}, - {file = "protobuf-4.24.0.tar.gz", hash = "sha256:5d0ceb9de6e08311832169e601d1fc71bd8e8c779f3ee38a97a78554945ecb85"}, + {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"}, ] [package.source] @@ -5018,6 +5249,152 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "pydantic" +version = "2.5.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.2-py3-none-any.whl", hash = "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0"}, + {file = "pydantic-2.5.2.tar.gz", hash = "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.5" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "pydantic-core" +version = "2.14.5" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd"}, + {file = "pydantic_core-2.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113"}, + {file = "pydantic_core-2.14.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997"}, + {file = "pydantic_core-2.14.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093"}, + {file = "pydantic_core-2.14.5-cp310-none-win32.whl", hash = "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720"}, + {file = "pydantic_core-2.14.5-cp310-none-win_amd64.whl", hash = "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459"}, + {file = "pydantic_core-2.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6"}, + {file = "pydantic_core-2.14.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada"}, + {file = "pydantic_core-2.14.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda"}, + {file = "pydantic_core-2.14.5-cp311-none-win32.whl", hash = "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651"}, + {file = "pydantic_core-2.14.5-cp311-none-win_amd64.whl", hash = "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077"}, + {file = "pydantic_core-2.14.5-cp311-none-win_arm64.whl", hash = "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093"}, + {file = "pydantic_core-2.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc"}, + {file = "pydantic_core-2.14.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69"}, + {file = "pydantic_core-2.14.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d"}, + {file = "pydantic_core-2.14.5-cp312-none-win32.whl", hash = "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260"}, + {file = "pydantic_core-2.14.5-cp312-none-win_amd64.whl", hash = "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36"}, + {file = "pydantic_core-2.14.5-cp312-none-win_arm64.whl", hash = "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4"}, + {file = "pydantic_core-2.14.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325"}, + {file = "pydantic_core-2.14.5-cp37-none-win32.whl", hash = "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405"}, + {file = "pydantic_core-2.14.5-cp37-none-win_amd64.whl", hash = "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf"}, + {file = "pydantic_core-2.14.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331"}, + {file = "pydantic_core-2.14.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec"}, + {file = "pydantic_core-2.14.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124"}, + {file = "pydantic_core-2.14.5-cp38-none-win32.whl", hash = "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867"}, + {file = "pydantic_core-2.14.5-cp38-none-win_amd64.whl", hash = "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7"}, + {file = "pydantic_core-2.14.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db"}, + {file = "pydantic_core-2.14.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5"}, + {file = "pydantic_core-2.14.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209"}, + {file = "pydantic_core-2.14.5-cp39-none-win32.whl", hash = "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6"}, + {file = "pydantic_core-2.14.5-cp39-none-win_amd64.whl", hash = "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7"}, + {file = "pydantic_core-2.14.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634"}, + {file = "pydantic_core-2.14.5-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8"}, + {file = "pydantic_core-2.14.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe"}, + {file = "pydantic_core-2.14.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3"}, + {file = "pydantic_core-2.14.5.tar.gz", hash = "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "pyexcel" version = "0.7.0" @@ -5137,17 +5514,18 @@ reference = "tsinghua" [[package]] name = "pygments" -version = "2.16.1" +version = "2.17.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.7" files = [ - {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, - {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] [package.extras] plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] [package.source] type = "legacy" @@ -5341,9 +5719,11 @@ 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"}, @@ -5359,11 +5739,13 @@ 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"}, ] @@ -5503,27 +5885,18 @@ reference = "tsinghua" [[package]] name = "pyspnego" -version = "0.9.1" +version = "0.10.2" description = "Windows Negotiate Authentication Client and Server" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pyspnego-0.9.1-cp310-cp310-win32.whl", hash = "sha256:aa43f00ed1c3b8e16a658613e2557a3ff9bea26acef867705eb4ee7f5e469ac3"}, - {file = "pyspnego-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:d031a7fa9c9ab3b67725e35affc90f8e6504518fb3ffe21573504e72b5a2fb5e"}, - {file = "pyspnego-0.9.1-cp311-cp311-win32.whl", hash = "sha256:58a17f7ba17f6cee72149911df6cc785ce7072744a386483957b74c62da654d8"}, - {file = "pyspnego-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:e7a92321272e3613c30f55a3324ef6d780bba8b723be8d50c35aac8409fc4028"}, - {file = "pyspnego-0.9.1-cp37-cp37m-win32.whl", hash = "sha256:87a2c23e640f4f6ae3c391d1f56e287b72908080a0e6376f2f365da5a2117dca"}, - {file = "pyspnego-0.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ff4fecf488369d6634afb20f9e56eb3b187fb3c883d6551601bbad4f4badab62"}, - {file = "pyspnego-0.9.1-cp38-cp38-win32.whl", hash = "sha256:7515f00418324809eb1adec0afac93da006c03baba6c6fd1a981b5401b798f56"}, - {file = "pyspnego-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:6366e39ba251889e2573c7d037e7feec8af86aea6c3b32f22a8af33bf88265b6"}, - {file = "pyspnego-0.9.1-cp39-cp39-win32.whl", hash = "sha256:07417f90328fb57c19a383e59b65060d4fc101441b74c34dbe4ba860775b0a3a"}, - {file = "pyspnego-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:4b79d5ba55ada38833d2421c44ed30a7313c00cbf34fa919dd106049616307d3"}, - {file = "pyspnego-0.9.1-py3-none-any.whl", hash = "sha256:c6aebe1fdc3990be2c137f3c3e041062243871b8161bc7adf4d269c3b6deda35"}, - {file = "pyspnego-0.9.1.tar.gz", hash = "sha256:6eea64f511bdfa192c2f80593ddf124258b0ea560327468953d18420e0ab3597"}, + {file = "pyspnego-0.10.2-py3-none-any.whl", hash = "sha256:3d5c5c28dbd0cd6a679acf45219630254db3c0e5ad4a16de521caa0585b088c0"}, + {file = "pyspnego-0.10.2.tar.gz", hash = "sha256:9a22c23aeae7b4424fdb2482450d3f8302ac012e2644e1cfe735cf468fcd12ed"}, ] [package.dependencies] cryptography = "*" +sspilib = {version = ">=0.1.0", markers = "sys_platform == \"win32\""} [package.extras] kerberos = ["gssapi (>=1.6.0)", "krb5 (>=0.3.0)"] @@ -5861,6 +6234,7 @@ 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"}, @@ -5868,8 +6242,15 @@ 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"}, @@ -5886,6 +6267,7 @@ 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"}, @@ -5893,6 +6275,7 @@ 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"}, @@ -6185,19 +6568,19 @@ reference = "tsinghua" [[package]] name = "setuptools" -version = "68.1.0" +version = "69.0.2" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.1.0-py3-none-any.whl", hash = "sha256:e13e1b0bc760e9b0127eda042845999b2f913e12437046e663b833aa96d89715"}, - {file = "setuptools-68.1.0.tar.gz", hash = "sha256:d59c97e7b774979a5ccb96388efc9eb65518004537e85d52e81eaee89ab6dd91"}, + {file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"}, + {file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [package.source] type = "legacy" @@ -6320,14 +6703,30 @@ url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" [[package]] -name = "soupsieve" -version = "2.4.1" -description = "A modern CSS selector implementation for Beautiful Soup." +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, - {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "soupsieve" +version = "2.5" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.8" +files = [ + {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, + {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, ] [package.source] @@ -6403,15 +6802,59 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" +[[package]] +name = "sspilib" +version = "0.1.0" +description = "SSPI API bindings for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "sspilib-0.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e43f3e684e9d29c80324bd54f52dac65ac4b18d81a2dcd529dce3994369a14d"}, + {file = "sspilib-0.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eb34eda5d362b6603707a55751f1eff81775709b821e51cb64d1d2fa2bb8b6e"}, + {file = "sspilib-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffe123f056f78cbe18aaed6b15f06e252020061c3387a72615abd46699a0b24"}, + {file = "sspilib-0.1.0-cp310-cp310-win32.whl", hash = "sha256:a4151072e28ec3b7d785beac9548a3d6a4549c431eb5487a5b8a1de028e9fef0"}, + {file = "sspilib-0.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2a19696c7b96b6bbef2b2ddf35df5a92f09b268476a348390a2f0da18cf29510"}, + {file = "sspilib-0.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:d2778e5e2881405b4d359a604e2802f5b7a7ed433ff62d6073d04c203af10eb1"}, + {file = "sspilib-0.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09d7f72ad5e4bbf9a8f1acf0d5f0c3f9fbe500f44c4a45ac24a99ece84f5654f"}, + {file = "sspilib-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e5705e11aaa030a61d2b0a2ce09d2b8a1962dd950e55adc7a3c87dd463c6878"}, + {file = "sspilib-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dced8213d311c56f5f38044716ebff5412cc156f19678659e8ffa9bb6a642bd7"}, + {file = "sspilib-0.1.0-cp311-cp311-win32.whl", hash = "sha256:d30d38d52dbd857732224e86ae3627d003cc510451083c69fa481fc7de88a7b6"}, + {file = "sspilib-0.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:61c9067168cce962f7fead42c28804c3a39a164b9a7b660200b8cfe31e3af071"}, + {file = "sspilib-0.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b526b8e5a236553f5137b951b89a2f108f56138ad05f31fd0a51b10f80b6c3cc"}, + {file = "sspilib-0.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3ff356d40cd34c900f94f1591eaabd458284042af611ebc1dbf609002066dba5"}, + {file = "sspilib-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b0fee3a52d0acef090f6c9b49953a8400fdc1c10aca7334319414a3038aa493"}, + {file = "sspilib-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab52d190dad1d578ec40d1fb417a8571954f4e32f35442a14cb709f57d3acbc9"}, + {file = "sspilib-0.1.0-cp312-cp312-win32.whl", hash = "sha256:b3cf819094383ec883e9a63c11b81d622618c815c18a6c9d761d9a14d9f028d1"}, + {file = "sspilib-0.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b83825a2c43ff84ddff72d09b098057efaabf3841d3c42888078e154cf8e9595"}, + {file = "sspilib-0.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:9aa6ab4c3fc1057251cf1f3f199daf90b99599cdfafc9eade8fdf0c01526dec8"}, + {file = "sspilib-0.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:82bff5df178386027d0112458b6971bbd18c76eb9e7be53fd61dab33d7bf8417"}, + {file = "sspilib-0.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:18393a9e6e0447cb7f319d361b65e9a0eaa5484705f16787133ffc49ad364c28"}, + {file = "sspilib-0.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a423fbca206ba0ca811dc995d8c3af045402b7d330f033e938b24f3a1d93fc"}, + {file = "sspilib-0.1.0-cp38-cp38-win32.whl", hash = "sha256:86bd936b1ef0aa63c6d9623ad08473e74ceb15f342f6e92cbade15ed9574cd33"}, + {file = "sspilib-0.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f688b94f0a64128444063e1d3d59152614175999222f6e2920681faea833f4"}, + {file = "sspilib-0.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2acef24e13e40d9dd8697eaae84ead9f417528ff741d087ec4eb4260518f4dc7"}, + {file = "sspilib-0.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b625802d80144d856d5eb6e8f4412f186565758da4493c7ad1b88e3d6d353de"}, + {file = "sspilib-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06ca1e34702bca1c750dcb5133b716f316b38dccb28d55a1a44d9842bc3f391"}, + {file = "sspilib-0.1.0-cp39-cp39-win32.whl", hash = "sha256:68496c9bd52b57a1b6d2e5529b43c30060249b8db901127b8343c4ad8cd93670"}, + {file = "sspilib-0.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:369727097f07a440099882580e284e137d9c27b7de354d63b65e327a454e7bee"}, + {file = "sspilib-0.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:87d8268c0517149c51a53b3888961ebf66826bb3dbb82c4e5cf10108f5456104"}, + {file = "sspilib-0.1.0.tar.gz", hash = "sha256:58b5291553cf6220549c0f855e0e6973f4977375d8236ce47bb581efb3e9b1cf"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + [[package]] name = "stack-data" -version = "0.6.2" +version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" optional = false python-versions = "*" files = [ - {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, - {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, ] [package.dependencies] @@ -6486,13 +6929,13 @@ reference = "tsinghua" [[package]] name = "texttable" -version = "1.6.7" +version = "1.7.0" description = "module to create simple ASCII tables" optional = false python-versions = "*" files = [ - {file = "texttable-1.6.7-py2.py3-none-any.whl", hash = "sha256:b7b68139aa8a6339d2c320ca8b1dc42d13a7831a346b446cb9eb385f0c76310c"}, - {file = "texttable-1.6.7.tar.gz", hash = "sha256:290348fb67f7746931bcdfd55ac7584ecd4e5b0846ab164333f0794b121760f2"}, + {file = "texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917"}, + {file = "texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638"}, ] [package.source] @@ -6526,19 +6969,44 @@ url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "tsinghua" [[package]] -name = "traitlets" -version = "5.9.0" -description = "Traitlets Python configuration system" +name = "tqdm" +version = "4.66.1" +description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, - {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, + {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, + {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "tsinghua" + +[[package]] +name = "traitlets" +version = "5.14.0" +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"}, ] [package.extras] docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<7.5)", "pytest-mock", "pytest-mypy-testing"] [package.source] type = "legacy" @@ -6566,44 +7034,42 @@ reference = "tsinghua" [[package]] name = "twisted" -version = "22.10.0" +version = "23.10.0" description = "An asynchronous networking framework written in Python" optional = false -python-versions = ">=3.7.1" +python-versions = ">=3.8.0" files = [ - {file = "Twisted-22.10.0-py3-none-any.whl", hash = "sha256:86c55f712cc5ab6f6d64e02503352464f0400f66d4f079096d744080afcccbd0"}, - {file = "Twisted-22.10.0.tar.gz", hash = "sha256:32acbd40a94f5f46e7b42c109bfae2b302250945561783a8b7a059048f2d4d31"}, + {file = "twisted-23.10.0-py3-none-any.whl", hash = "sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444"}, + {file = "twisted-23.10.0.tar.gz", hash = "sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5"}, ] [package.dependencies] -attrs = ">=19.2.0" -Automat = ">=0.8.0" +attrs = ">=21.3.0" +automat = ">=0.8.0" constantly = ">=15.1" hyperlink = ">=17.1.1" idna = {version = ">=2.4", optional = true, markers = "extra == \"tls\""} -incremental = ">=21.3.0" +incremental = ">=22.10.0" pyopenssl = {version = ">=21.0.0", optional = true, markers = "extra == \"tls\""} service-identity = {version = ">=18.1.0", optional = true, markers = "extra == \"tls\""} twisted-iocpsupport = {version = ">=1.0.2,<2", markers = "platform_system == \"Windows\""} -typing-extensions = ">=3.6.5" -"zope.interface" = ">=4.4.2" +typing-extensions = ">=4.2.0" +zope-interface = ">=5" [package.extras] -all-non-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] -conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"] -conch-nacl = ["PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "cryptography (>=2.6)", "pyasn1"] -contextvars = ["contextvars (>=2.4,<3)"] -dev = ["coverage (>=6b1,<7)", "pydoctor (>=22.9.0,<22.10.0)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)", "twistedchecker (>=0.7,<1.0)"] -dev-release = ["pydoctor (>=22.9.0,<22.10.0)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)"] -gtk-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pygobject", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] +all-non-platform = ["twisted[conch,http2,serial,test,tls]", "twisted[conch,http2,serial,test,tls]"] +conch = ["appdirs (>=1.4.0)", "bcrypt (>=3.1.3)", "cryptography (>=3.3)"] +dev = ["coverage (>=6b1,<7)", "pyflakes (>=2.2,<3.0)", "python-subunit (>=1.4,<2.0)", "twisted[dev-release]", "twistedchecker (>=0.7,<1.0)"] +dev-release = ["pydoctor (>=23.9.0,<23.10.0)", "pydoctor (>=23.9.0,<23.10.0)", "sphinx (>=6,<7)", "sphinx (>=6,<7)", "sphinx-rtd-theme (>=1.3,<2.0)", "sphinx-rtd-theme (>=1.3,<2.0)", "towncrier (>=23.6,<24.0)", "towncrier (>=23.6,<24.0)"] +gtk-platform = ["pygobject", "pygobject", "twisted[all-non-platform]", "twisted[all-non-platform]"] http2 = ["h2 (>=3.0,<5.0)", "priority (>=1.1.0,<2.0)"] -macos-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] -mypy = ["PyHamcrest (>=1.9.0)", "PyNaCl", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "coverage (>=6b1,<7)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "mypy (==0.930)", "mypy-zope (==0.3.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pydoctor (>=22.9.0,<22.10.0)", "pyflakes (>=2.2,<3.0)", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "python-subunit (>=1.4,<2.0)", "pywin32 (!=226)", "readthedocs-sphinx-ext (>=2.1,<3.0)", "service-identity (>=18.1.0)", "sphinx (>=5.0,<6)", "sphinx-rtd-theme (>=1.0,<2.0)", "towncrier (>=22.8,<23.0)", "twistedchecker (>=0.7,<1.0)", "types-pyOpenSSL", "types-setuptools"] -osx-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyobjc-core", "pyobjc-framework-CFNetwork", "pyobjc-framework-Cocoa", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] +macos-platform = ["pyobjc-core", "pyobjc-core", "pyobjc-framework-cfnetwork", "pyobjc-framework-cfnetwork", "pyobjc-framework-cocoa", "pyobjc-framework-cocoa", "twisted[all-non-platform]", "twisted[all-non-platform]"] +mypy = ["mypy (>=1.5.1,<1.6.0)", "mypy-zope (>=1.0.1,<1.1.0)", "twisted[all-non-platform,dev]", "types-pyopenssl", "types-setuptools"] +osx-platform = ["twisted[macos-platform]", "twisted[macos-platform]"] serial = ["pyserial (>=3.0)", "pywin32 (!=226)"] -test = ["PyHamcrest (>=1.9.0)", "cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.0,<7.0)"] +test = ["cython-test-exception-raiser (>=1.0.2,<2)", "hypothesis (>=6.56)", "pyhamcrest (>=2)"] tls = ["idna (>=2.4)", "pyopenssl (>=21.0.0)", "service-identity (>=18.1.0)"] -windows-platform = ["PyHamcrest (>=1.9.0)", "appdirs (>=1.4.0)", "bcrypt (>=3.0.0)", "contextvars (>=2.4,<3)", "cryptography (>=2.6)", "cython-test-exception-raiser (>=1.0.2,<2)", "h2 (>=3.0,<5.0)", "hypothesis (>=6.0,<7.0)", "idna (>=2.4)", "priority (>=1.1.0,<2.0)", "pyasn1", "pyopenssl (>=21.0.0)", "pyserial (>=3.0)", "pywin32 (!=226)", "pywin32 (!=226)", "service-identity (>=18.1.0)"] +windows-platform = ["pywin32 (!=226)", "pywin32 (!=226)", "twisted[all-non-platform]", "twisted[all-non-platform]"] [package.source] type = "legacy" @@ -6666,13 +7132,13 @@ reference = "tsinghua" [[package]] name = "typing-extensions" -version = "4.7.1" -description = "Backported and Experimental Type Hints for Python 3.7+" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, - {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, ] [package.source] @@ -6849,13 +7315,13 @@ reference = "tsinghua" [[package]] name = "wcwidth" -version = "0.2.6" +version = "0.2.12" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" files = [ - {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, - {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, + {file = "wcwidth-0.2.12-py2.py3-none-any.whl", hash = "sha256:f26ec43d96c8cbfed76a5075dac87680124fa84e0855195a6184da9c187f133c"}, + {file = "wcwidth-0.2.12.tar.gz", hash = "sha256:f01c104efdf57971bcb756f054dd58ddec5204dd15fa31d6503ea57947d97c02"}, ] [package.source] @@ -6992,86 +7458,81 @@ reference = "tsinghua" [[package]] name = "wrapt" -version = "1.15.0" +version = "1.16.0" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" files = [ - {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, - {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, - {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, - {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, - {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, - {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, - {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, - {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, - {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, - {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, - {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, - {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, - {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, - {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, - {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, - {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, - {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, - {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, - {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, - {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, - {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, - {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, - {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, - {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, - {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, - {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, - {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, - {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, - {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, - {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, - {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, - {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, - {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, - {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, - {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, - {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, - {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, - {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] [package.source] @@ -7127,85 +7588,101 @@ reference = "tsinghua" [[package]] name = "yarl" -version = "1.9.2" +version = "1.9.3" description = "Yet another URL library" optional = false python-versions = ">=3.7" files = [ - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8c2ad583743d16ddbdf6bb14b5cd76bf43b0d0006e918809d5d4ddf7bde8dd82"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:82aa6264b36c50acfb2424ad5ca537a2060ab6de158a5bd2a72a032cc75b9eb8"}, - {file = "yarl-1.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c0c77533b5ed4bcc38e943178ccae29b9bcf48ffd1063f5821192f23a1bd27b9"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee4afac41415d52d53a9833ebae7e32b344be72835bbb589018c9e938045a560"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9bf345c3a4f5ba7f766430f97f9cc1320786f19584acc7086491f45524a551ac"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a96c19c52ff442a808c105901d0bdfd2e28575b3d5f82e2f5fd67e20dc5f4ea"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:891c0e3ec5ec881541f6c5113d8df0315ce5440e244a716b95f2525b7b9f3608"}, - {file = "yarl-1.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c3a53ba34a636a256d767c086ceb111358876e1fb6b50dfc4d3f4951d40133d5"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:566185e8ebc0898b11f8026447eacd02e46226716229cea8db37496c8cdd26e0"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2b0738fb871812722a0ac2154be1f049c6223b9f6f22eec352996b69775b36d4"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:32f1d071b3f362c80f1a7d322bfd7b2d11e33d2adf395cc1dd4df36c9c243095"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:e9fdc7ac0d42bc3ea78818557fab03af6181e076a2944f43c38684b4b6bed8e3"}, - {file = "yarl-1.9.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56ff08ab5df8429901ebdc5d15941b59f6253393cb5da07b4170beefcf1b2528"}, - {file = "yarl-1.9.2-cp310-cp310-win32.whl", hash = "sha256:8ea48e0a2f931064469bdabca50c2f578b565fc446f302a79ba6cc0ee7f384d3"}, - {file = "yarl-1.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:50f33040f3836e912ed16d212f6cc1efb3231a8a60526a407aeb66c1c1956dde"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:646d663eb2232d7909e6601f1a9107e66f9791f290a1b3dc7057818fe44fc2b6"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aff634b15beff8902d1f918012fc2a42e0dbae6f469fce134c8a0dc51ca423bb"}, - {file = "yarl-1.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a83503934c6273806aed765035716216cc9ab4e0364f7f066227e1aaea90b8d0"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b25322201585c69abc7b0e89e72790469f7dad90d26754717f3310bfe30331c2"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22a94666751778629f1ec4280b08eb11815783c63f52092a5953faf73be24191"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ec53a0ea2a80c5cd1ab397925f94bff59222aa3cf9c6da938ce05c9ec20428d"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:159d81f22d7a43e6eabc36d7194cb53f2f15f498dbbfa8edc8a3239350f59fe7"}, - {file = "yarl-1.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:832b7e711027c114d79dffb92576acd1bd2decc467dec60e1cac96912602d0e6"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:95d2ecefbcf4e744ea952d073c6922e72ee650ffc79028eb1e320e732898d7e8"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d4e2c6d555e77b37288eaf45b8f60f0737c9efa3452c6c44626a5455aeb250b9"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:783185c75c12a017cc345015ea359cc801c3b29a2966c2655cd12b233bf5a2be"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:b8cc1863402472f16c600e3e93d542b7e7542a540f95c30afd472e8e549fc3f7"}, - {file = "yarl-1.9.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:822b30a0f22e588b32d3120f6d41e4ed021806418b4c9f0bc3048b8c8cb3f92a"}, - {file = "yarl-1.9.2-cp311-cp311-win32.whl", hash = "sha256:a60347f234c2212a9f0361955007fcf4033a75bf600a33c88a0a8e91af77c0e8"}, - {file = "yarl-1.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:be6b3fdec5c62f2a67cb3f8c6dbf56bbf3f61c0f046f84645cd1ca73532ea051"}, - {file = "yarl-1.9.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:38a3928ae37558bc1b559f67410df446d1fbfa87318b124bf5032c31e3447b74"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac9bb4c5ce3975aeac288cfcb5061ce60e0d14d92209e780c93954076c7c4367"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3da8a678ca8b96c8606bbb8bfacd99a12ad5dd288bc6f7979baddd62f71c63ef"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13414591ff516e04fcdee8dc051c13fd3db13b673c7a4cb1350e6b2ad9639ad3"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf74d08542c3a9ea97bb8f343d4fcbd4d8f91bba5ec9d5d7f792dbe727f88938"}, - {file = "yarl-1.9.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7221580dc1db478464cfeef9b03b95c5852cc22894e418562997df0d074ccc"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:494053246b119b041960ddcd20fd76224149cfea8ed8777b687358727911dd33"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:52a25809fcbecfc63ac9ba0c0fb586f90837f5425edfd1ec9f3372b119585e45"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:e65610c5792870d45d7b68c677681376fcf9cc1c289f23e8e8b39c1485384185"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:1b1bba902cba32cdec51fca038fd53f8beee88b77efc373968d1ed021024cc04"}, - {file = "yarl-1.9.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:662e6016409828ee910f5d9602a2729a8a57d74b163c89a837de3fea050c7582"}, - {file = "yarl-1.9.2-cp37-cp37m-win32.whl", hash = "sha256:f364d3480bffd3aa566e886587eaca7c8c04d74f6e8933f3f2c996b7f09bee1b"}, - {file = "yarl-1.9.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6a5883464143ab3ae9ba68daae8e7c5c95b969462bbe42e2464d60e7e2698368"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5610f80cf43b6202e2c33ba3ec2ee0a2884f8f423c8f4f62906731d876ef4fac"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b9a4e67ad7b646cd6f0938c7ebfd60e481b7410f574c560e455e938d2da8e0f4"}, - {file = "yarl-1.9.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:83fcc480d7549ccebe9415d96d9263e2d4226798c37ebd18c930fce43dfb9574"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fcd436ea16fee7d4207c045b1e340020e58a2597301cfbcfdbe5abd2356c2fb"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84e0b1599334b1e1478db01b756e55937d4614f8654311eb26012091be109d59"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3458a24e4ea3fd8930e934c129b676c27452e4ebda80fbe47b56d8c6c7a63a9e"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:838162460b3a08987546e881a2bfa573960bb559dfa739e7800ceeec92e64417"}, - {file = "yarl-1.9.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f4e2d08f07a3d7d3e12549052eb5ad3eab1c349c53ac51c209a0e5991bbada78"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de119f56f3c5f0e2fb4dee508531a32b069a5f2c6e827b272d1e0ff5ac040333"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:149ddea5abf329752ea5051b61bd6c1d979e13fbf122d3a1f9f0c8be6cb6f63c"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:674ca19cbee4a82c9f54e0d1eee28116e63bc6fd1e96c43031d11cbab8b2afd5"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:9b3152f2f5677b997ae6c804b73da05a39daa6a9e85a512e0e6823d81cdad7cc"}, - {file = "yarl-1.9.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415d5a4b080dc9612b1b63cba008db84e908b95848369aa1da3686ae27b6d2b"}, - {file = "yarl-1.9.2-cp38-cp38-win32.whl", hash = "sha256:f7a3d8146575e08c29ed1cd287068e6d02f1c7bdff8970db96683b9591b86ee7"}, - {file = "yarl-1.9.2-cp38-cp38-win_amd64.whl", hash = "sha256:63c48f6cef34e6319a74c727376e95626f84ea091f92c0250a98e53e62c77c72"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75df5ef94c3fdc393c6b19d80e6ef1ecc9ae2f4263c09cacb178d871c02a5ba9"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c027a6e96ef77d401d8d5a5c8d6bc478e8042f1e448272e8d9752cb0aff8b5c8"}, - {file = "yarl-1.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3b078dbe227f79be488ffcfc7a9edb3409d018e0952cf13f15fd6512847f3f7"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59723a029760079b7d991a401386390c4be5bfec1e7dd83e25a6a0881859e716"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b03917871bf859a81ccb180c9a2e6c1e04d2f6a51d953e6a5cdd70c93d4e5a2a"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1012fa63eb6c032f3ce5d2171c267992ae0c00b9e164efe4d73db818465fac3"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a74dcbfe780e62f4b5a062714576f16c2f3493a0394e555ab141bf0d746bb955"}, - {file = "yarl-1.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c56986609b057b4839968ba901944af91b8e92f1725d1a2d77cbac6972b9ed1"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2c315df3293cd521033533d242d15eab26583360b58f7ee5d9565f15fee1bef4"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b7232f8dfbd225d57340e441d8caf8652a6acd06b389ea2d3222b8bc89cbfca6"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:53338749febd28935d55b41bf0bcc79d634881195a39f6b2f767870b72514caf"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:066c163aec9d3d073dc9ffe5dd3ad05069bcb03fcaab8d221290ba99f9f69ee3"}, - {file = "yarl-1.9.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8288d7cd28f8119b07dd49b7230d6b4562f9b61ee9a4ab02221060d21136be80"}, - {file = "yarl-1.9.2-cp39-cp39-win32.whl", hash = "sha256:b124e2a6d223b65ba8768d5706d103280914d61f5cae3afbc50fc3dfcc016623"}, - {file = "yarl-1.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:61016e7d582bc46a5378ffdd02cd0314fb8ba52f40f9cf4d9a5e7dbef88dee18"}, - {file = "yarl-1.9.2.tar.gz", hash = "sha256:04ab9d4b9f587c06d801c2abfe9317b77cdf996c65a90d5e84ecc45010823571"}, + {file = "yarl-1.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32435d134414e01d937cd9d6cc56e8413a8d4741dea36af5840c7750f04d16ab"}, + {file = "yarl-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a5211de242754b5e612557bca701f39f8b1a9408dff73c6db623f22d20f470e"}, + {file = "yarl-1.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:525cd69eff44833b01f8ef39aa33a9cc53a99ff7f9d76a6ef6a9fb758f54d0ff"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc94441bcf9cb8c59f51f23193316afefbf3ff858460cb47b5758bf66a14d130"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e36021db54b8a0475805acc1d6c4bca5d9f52c3825ad29ae2d398a9d530ddb88"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0f17d1df951336a02afc8270c03c0c6e60d1f9996fcbd43a4ce6be81de0bd9d"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5f3faeb8100a43adf3e7925d556801d14b5816a0ac9e75e22948e787feec642"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aed37db837ecb5962469fad448aaae0f0ee94ffce2062cf2eb9aed13328b5196"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:721ee3fc292f0d069a04016ef2c3a25595d48c5b8ddc6029be46f6158d129c92"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b8bc5b87a65a4e64bc83385c05145ea901b613d0d3a434d434b55511b6ab0067"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:dd952b9c64f3b21aedd09b8fe958e4931864dba69926d8a90c90d36ac4e28c9a"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:c405d482c320a88ab53dcbd98d6d6f32ada074f2d965d6e9bf2d823158fa97de"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9df9a0d4c5624790a0dea2e02e3b1b3c69aed14bcb8650e19606d9df3719e87d"}, + {file = "yarl-1.9.3-cp310-cp310-win32.whl", hash = "sha256:d34c4f80956227f2686ddea5b3585e109c2733e2d4ef12eb1b8b4e84f09a2ab6"}, + {file = "yarl-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:cf7a4e8de7f1092829caef66fd90eaf3710bc5efd322a816d5677b7664893c93"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d61a0ca95503867d4d627517bcfdc28a8468c3f1b0b06c626f30dd759d3999fd"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73cc83f918b69110813a7d95024266072d987b903a623ecae673d1e71579d566"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d81657b23e0edb84b37167e98aefb04ae16cbc5352770057893bd222cdc6e45f"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a1a8443091c7fbc17b84a0d9f38de34b8423b459fb853e6c8cdfab0eacf613"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe34befb8c765b8ce562f0200afda3578f8abb159c76de3ab354c80b72244c41"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c757f64afe53a422e45e3e399e1e3cf82b7a2f244796ce80d8ca53e16a49b9f"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a57b41a0920b9a220125081c1e191b88a4cdec13bf9d0649e382a822705c65"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632c7aeb99df718765adf58eacb9acb9cbc555e075da849c1378ef4d18bf536a"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b0b8c06afcf2bac5a50b37f64efbde978b7f9dc88842ce9729c020dc71fae4ce"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1d93461e2cf76c4796355494f15ffcb50a3c198cc2d601ad8d6a96219a10c363"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4003f380dac50328c85e85416aca6985536812c082387255c35292cb4b41707e"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4d6d74a97e898c1c2df80339aa423234ad9ea2052f66366cef1e80448798c13d"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b61e64b06c3640feab73fa4ff9cb64bd8182de52e5dc13038e01cfe674ebc321"}, + {file = "yarl-1.9.3-cp311-cp311-win32.whl", hash = "sha256:29beac86f33d6c7ab1d79bd0213aa7aed2d2f555386856bb3056d5fdd9dab279"}, + {file = "yarl-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:f7271d6bd8838c49ba8ae647fc06469137e1c161a7ef97d778b72904d9b68696"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dd318e6b75ca80bff0b22b302f83a8ee41c62b8ac662ddb49f67ec97e799885d"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c4b1efb11a8acd13246ffb0bee888dd0e8eb057f8bf30112e3e21e421eb82d4a"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f034386e5550b5dc8ded90b5e2ff7db21f0f5c7de37b6efc5dac046eb19c10"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd49a908cb6d387fc26acee8b7d9fcc9bbf8e1aca890c0b2fdfd706057546080"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa4643635f26052401750bd54db911b6342eb1a9ac3e74f0f8b58a25d61dfe41"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e741bd48e6a417bdfbae02e088f60018286d6c141639359fb8df017a3b69415a"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c86d0d0919952d05df880a1889a4f0aeb6868e98961c090e335671dea5c0361"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d5434b34100b504aabae75f0622ebb85defffe7b64ad8f52b8b30ec6ef6e4b9"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79e1df60f7c2b148722fb6cafebffe1acd95fd8b5fd77795f56247edaf326752"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:44e91a669c43f03964f672c5a234ae0d7a4d49c9b85d1baa93dec28afa28ffbd"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3cfa4dbe17b2e6fca1414e9c3bcc216f6930cb18ea7646e7d0d52792ac196808"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:88d2c3cc4b2f46d1ba73d81c51ec0e486f59cc51165ea4f789677f91a303a9a7"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cccdc02e46d2bd7cb5f38f8cc3d9db0d24951abd082b2f242c9e9f59c0ab2af3"}, + {file = "yarl-1.9.3-cp312-cp312-win32.whl", hash = "sha256:96758e56dceb8a70f8a5cff1e452daaeff07d1cc9f11e9b0c951330f0a2396a7"}, + {file = "yarl-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:c4472fe53ebf541113e533971bd8c32728debc4c6d8cc177f2bff31d011ec17e"}, + {file = "yarl-1.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:126638ab961633f0940a06e1c9d59919003ef212a15869708dcb7305f91a6732"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99ddaddb2fbe04953b84d1651149a0d85214780e4d0ee824e610ab549d98d92"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dab30b21bd6fb17c3f4684868c7e6a9e8468078db00f599fb1c14e324b10fca"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:828235a2a169160ee73a2fcfb8a000709edf09d7511fccf203465c3d5acc59e4"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc391e3941045fd0987c77484b2799adffd08e4b6735c4ee5f054366a2e1551d"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51382c72dd5377861b573bd55dcf680df54cea84147c8648b15ac507fbef984d"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:28a108cb92ce6cf867690a962372996ca332d8cda0210c5ad487fe996e76b8bb"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8f18a7832ff85dfcd77871fe677b169b1bc60c021978c90c3bb14f727596e0ae"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:7eaf13af79950142ab2bbb8362f8d8d935be9aaf8df1df89c86c3231e4ff238a"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:66a6dbf6ca7d2db03cc61cafe1ee6be838ce0fbc97781881a22a58a7c5efef42"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1a0a4f3aaa18580038cfa52a7183c8ffbbe7d727fe581300817efc1e96d1b0e9"}, + {file = "yarl-1.9.3-cp37-cp37m-win32.whl", hash = "sha256:946db4511b2d815979d733ac6a961f47e20a29c297be0d55b6d4b77ee4b298f6"}, + {file = "yarl-1.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2dad8166d41ebd1f76ce107cf6a31e39801aee3844a54a90af23278b072f1ccf"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bb72d2a94481e7dc7a0c522673db288f31849800d6ce2435317376a345728225"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a172c3d5447b7da1680a1a2d6ecdf6f87a319d21d52729f45ec938a7006d5d8"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2dc72e891672343b99db6d497024bf8b985537ad6c393359dc5227ef653b2f17"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8d51817cf4b8d545963ec65ff06c1b92e5765aa98831678d0e2240b6e9fd281"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53ec65f7eee8655bebb1f6f1607760d123c3c115a324b443df4f916383482a67"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cfd77e8e5cafba3fb584e0f4b935a59216f352b73d4987be3af51f43a862c403"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e73db54c967eb75037c178a54445c5a4e7461b5203b27c45ef656a81787c0c1b"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09c19e5f4404574fcfb736efecf75844ffe8610606f3fccc35a1515b8b6712c4"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6280353940f7e5e2efaaabd686193e61351e966cc02f401761c4d87f48c89ea4"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c25ec06e4241e162f5d1f57c370f4078797ade95c9208bd0c60f484834f09c96"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7217234b10c64b52cc39a8d82550342ae2e45be34f5bff02b890b8c452eb48d7"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4ce77d289f8d40905c054b63f29851ecbfd026ef4ba5c371a158cfe6f623663e"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5f74b015c99a5eac5ae589de27a1201418a5d9d460e89ccb3366015c6153e60a"}, + {file = "yarl-1.9.3-cp38-cp38-win32.whl", hash = "sha256:8a2538806be846ea25e90c28786136932ec385c7ff3bc1148e45125984783dc6"}, + {file = "yarl-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:6465d36381af057d0fab4e0f24ef0e80ba61f03fe43e6eeccbe0056e74aadc70"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2f3c8822bc8fb4a347a192dd6a28a25d7f0ea3262e826d7d4ef9cc99cd06d07e"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7831566595fe88ba17ea80e4b61c0eb599f84c85acaa14bf04dd90319a45b90"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff34cb09a332832d1cf38acd0f604c068665192c6107a439a92abfd8acf90fe2"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe8080b4f25dfc44a86bedd14bc4f9d469dfc6456e6f3c5d9077e81a5fedfba7"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8535e111a064f3bdd94c0ed443105934d6f005adad68dd13ce50a488a0ad1bf3"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d155a092bf0ebf4a9f6f3b7a650dc5d9a5bbb585ef83a52ed36ba46f55cc39d"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778df71c8d0c8c9f1b378624b26431ca80041660d7be7c3f724b2c7a6e65d0d6"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f9cafaf031c34d95c1528c16b2fa07b710e6056b3c4e2e34e9317072da5d1a"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ca6b66f69e30f6e180d52f14d91ac854b8119553b524e0e28d5291a724f0f423"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0e7e83f31e23c5d00ff618045ddc5e916f9e613d33c5a5823bc0b0a0feb522f"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:af52725c7c39b0ee655befbbab5b9a1b209e01bb39128dce0db226a10014aacc"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0ab5baaea8450f4a3e241ef17e3d129b2143e38a685036b075976b9c415ea3eb"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d350388ba1129bc867c6af1cd17da2b197dff0d2801036d2d7d83c2d771a682"}, + {file = "yarl-1.9.3-cp39-cp39-win32.whl", hash = "sha256:e2a16ef5fa2382af83bef4a18c1b3bcb4284c4732906aa69422cf09df9c59f1f"}, + {file = "yarl-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:d92d897cb4b4bf915fbeb5e604c7911021a8456f0964f3b8ebbe7f9188b9eabb"}, + {file = "yarl-1.9.3-py3-none-any.whl", hash = "sha256:271d63396460b6607b588555ea27a1a02b717ca2e3f2cf53bdde4013d7790929"}, + {file = "yarl-1.9.3.tar.gz", hash = "sha256:4a14907b597ec55740f63e52d7fee0e9ee09d5b9d57a4f399a7423268e457b57"}, ] [package.dependencies] @@ -7219,48 +7696,54 @@ reference = "tsinghua" [[package]] name = "zope-interface" -version = "6.0" +version = "6.1" description = "Interfaces for Python" optional = false python-versions = ">=3.7" files = [ - {file = "zope.interface-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f299c020c6679cb389814a3b81200fe55d428012c5e76da7e722491f5d205990"}, - {file = "zope.interface-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee4b43f35f5dc15e1fec55ccb53c130adb1d11e8ad8263d68b1284b66a04190d"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a158846d0fca0a908c1afb281ddba88744d403f2550dc34405c3691769cdd85"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f72f23bab1848edb7472309e9898603141644faec9fd57a823ea6b4d1c4c8995"}, - {file = "zope.interface-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48f4d38cf4b462e75fac78b6f11ad47b06b1c568eb59896db5b6ec1094eb467f"}, - {file = "zope.interface-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:87b690bbee9876163210fd3f500ee59f5803e4a6607d1b1238833b8885ebd410"}, - {file = "zope.interface-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2363e5fd81afb650085c6686f2ee3706975c54f331b426800b53531191fdf28"}, - {file = "zope.interface-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af169ba897692e9cd984a81cb0f02e46dacdc07d6cf9fd5c91e81f8efaf93d52"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa90bac61c9dc3e1a563e5babb3fd2c0c1c80567e815442ddbe561eadc803b30"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89086c9d3490a0f265a3c4b794037a84541ff5ffa28bb9c24cc9f66566968464"}, - {file = "zope.interface-6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:809fe3bf1a91393abc7e92d607976bbb8586512913a79f2bf7d7ec15bd8ea518"}, - {file = "zope.interface-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:0ec9653825f837fbddc4e4b603d90269b501486c11800d7c761eee7ce46d1bbb"}, - {file = "zope.interface-6.0-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:790c1d9d8f9c92819c31ea660cd43c3d5451df1df61e2e814a6f99cebb292788"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b39b8711578dcfd45fc0140993403b8a81e879ec25d53189f3faa1f006087dca"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eba51599370c87088d8882ab74f637de0c4f04a6d08a312dce49368ba9ed5c2a"}, - {file = "zope.interface-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee934f023f875ec2cfd2b05a937bd817efcc6c4c3f55c5778cbf78e58362ddc"}, - {file = "zope.interface-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:042f2381118b093714081fd82c98e3b189b68db38ee7d35b63c327c470ef8373"}, - {file = "zope.interface-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dfbbbf0809a3606046a41f8561c3eada9db811be94138f42d9135a5c47e75f6f"}, - {file = "zope.interface-6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:424d23b97fa1542d7be882eae0c0fc3d6827784105264a8169a26ce16db260d8"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e538f2d4a6ffb6edfb303ce70ae7e88629ac6e5581870e66c306d9ad7b564a58"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12175ca6b4db7621aedd7c30aa7cfa0a2d65ea3a0105393e05482d7a2d367446"}, - {file = "zope.interface-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c3d7dfd897a588ec27e391edbe3dd320a03684457470415870254e714126b1f"}, - {file = "zope.interface-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b3f543ae9d3408549a9900720f18c0194ac0fe810cecda2a584fd4dca2eb3bb8"}, - {file = "zope.interface-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0583b75f2e70ec93f100931660328965bb9ff65ae54695fb3fa0a1255daa6f2"}, - {file = "zope.interface-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:23ac41d52fd15dd8be77e3257bc51bbb82469cf7f5e9a30b75e903e21439d16c"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99856d6c98a326abbcc2363827e16bd6044f70f2ef42f453c0bd5440c4ce24e5"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1592f68ae11e557b9ff2bc96ac8fc30b187e77c45a3c9cd876e3368c53dc5ba8"}, - {file = "zope.interface-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4407b1435572e3e1610797c9203ad2753666c62883b921318c5403fb7139dec2"}, - {file = "zope.interface-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:5171eb073474a5038321409a630904fd61f12dd1856dd7e9d19cd6fe092cbbc5"}, - {file = "zope.interface-6.0.tar.gz", hash = "sha256:aab584725afd10c710b8f1e6e208dbee2d0ad009f57d674cb9d1b3964037275d"}, + {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, + {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, + {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, + {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, + {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, + {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, + {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, + {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, + {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, + {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, ] [package.dependencies] setuptools = "*" [package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface"] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] @@ -7272,4 +7755,4 @@ reference = "tsinghua" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "882df81581adc280c0349a83b88c42987598c2b125a21cf7c834f8a32e6fc3f3" +content-hash = "397cb294c81da3ce74f1a1c3044bd778669284c0c280177162ed9d51e623a7c8" diff --git a/pyproject.toml b/pyproject.toml index b071222d8..eff9f4444 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "jumpserver" -version = "v3.5" +version = "v3.9" description = "广受欢迎的开源堡垒机" authors = ["ibuler "] license = "GPLv3" @@ -102,7 +102,7 @@ pytz = "2023.3" django-proxy = "1.2.2" python-daemon = "3.0.1" eventlet = "0.33.3" -greenlet = "2.0.2" +greenlet = "3.0.1" gunicorn = "21.2.0" celery = "5.3.1" flower = "2.0.1" @@ -142,6 +142,9 @@ channels-redis = "4.1.0" fido2 = "^1.1.2" ua-parser = "^0.18.0" user-agents = "^2.2.0" +django-cors-headers = "^4.3.0" +mistune = "0.8.4" +openai = "^1.3.7" [tool.poetry.group.xpack.dependencies] diff --git a/utils/check_celery.sh b/utils/check_celery.sh index ba2a8777a..e84677081 100644 --- a/utils/check_celery.sh +++ b/utils/check_celery.sh @@ -4,5 +4,5 @@ set -e test -e /tmp/worker_ready_ansible test -e /tmp/worker_ready_celery -test -e /tmp/worker_heartbeat_ansible && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_ansible))) -lt 10 -test -e /tmp/worker_heartbeat_celery && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_celery))) -lt 10 +test -e /tmp/worker_heartbeat_ansible && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_ansible))) -lt 20 +test -e /tmp/worker_heartbeat_celery && test $(($(date +%s) - $(stat -c %Y /tmp/worker_heartbeat_celery))) -lt 20