diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index e47b9174..00000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - # Ruff version. - rev: v0.2.2 - hooks: - # Run the linter. - - id: ruff - args: [ --select, I, --fix ] - # Run the formatter. - - id: ruff-format - diff --git a/Dockerfile b/Dockerfile index de50f0fc..3c141f60 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,9 @@ -# 使用 python:3.11-buster 作为基础镜像 FROM python:3.11-buster as Base -# 安装 Poetry -RUN pip install poetry -# 复制 pyproject.toml 和 poetry.lock 文件 -COPY pyproject.toml poetry.lock ./ -# 使用 Poetry 生成 requirements.txt 文件 -RUN poetry export -f requirements.txt --output requirements.txt --without-hashes +COPY requirements.txt . -# 安装依赖 ARG DEBIAN_REPO="deb.debian.org" ARG PIP_INDEX_URL="https://pypi.org/simple" @@ -28,11 +21,9 @@ RUN apt-get update && \ apt-get autoremove -y && \ rm -rf /var/lib/apt/lists/* -# 使用 python:3.11-buster 作为基础镜像 FROM python:3.11-buster ENV TZ=Asia/Shanghai -# 设置时区 ARG DEBIAN_REPO="deb.debian.org" RUN echo "deb http://$DEBIAN_REPO/debian/ buster main contrib non-free" > /etc/apt/sources.list && \ echo "deb-src http://$DEBIAN_REPO/debian/ buster main contrib non-free" >> /etc/apt/sources.list && \ @@ -41,26 +32,19 @@ RUN echo "deb http://$DEBIAN_REPO/debian/ buster main contrib non-free" > /etc/a echo "deb http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list && \ echo "deb-src http://$DEBIAN_REPO/debian/ buster-updates main contrib non-free" >> /etc/apt/sources.list + RUN apt-get update && \ apt-get install -y tzdata && \ ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && \ echo $TZ > /etc/timezone && \ rm -rf /var/lib/apt/lists/* -# 复制依赖 COPY --from=Base /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages - -# 设置工作目录 WORKDIR /app - -# 复制项目文件 COPY . /app - -# 设置权限 RUN chmod +x /app/start.sh -# 收集静态文件 RUN python manage.py collectstatic --settings=FasterRunner.settings.docker --no-input -# 设置入口点 ENTRYPOINT ["/app/start.sh"] + diff --git a/FasterRunner/__init__.py b/FasterRunner/__init__.py index 216a1ae1..daae0a1f 100644 --- a/FasterRunner/__init__.py +++ b/FasterRunner/__init__.py @@ -3,3 +3,4 @@ # 替换mysqlclient pymysql.version_info = (1, 4, 6, "final", 0) # 修改版本号为兼容版本 pymysql.install_as_MySQLdb() + diff --git a/FasterRunner/auth.py b/FasterRunner/auth.py index b708931a..6f3bbb84 100644 --- a/FasterRunner/auth.py +++ b/FasterRunner/auth.py @@ -5,10 +5,7 @@ from django.utils.translation import gettext as _ from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication -from rest_framework_jwt.authentication import ( - JSONWebTokenAuthentication, - jwt_get_username_from_payload, -) +from rest_framework_jwt.authentication import JSONWebTokenAuthentication, jwt_get_username_from_payload from rest_framework_jwt.settings import api_settings jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER @@ -21,9 +18,11 @@ def is_admin(token): is_permission = models.MyUser.objects.filter(is_superuser=1).first() if not is_permission: - raise exceptions.PermissionDenied( - {"code": "9996", "msg": "权限不足,请联系管理员", "success": False} - ) + raise exceptions.PermissionDenied({ + "code": "9996", + "msg": "权限不足,请联系管理员", + "success": False + }) else: return True @@ -34,12 +33,12 @@ class OnlyGetAuthenticator(BaseAuthentication): """ def authenticate(self, request): - if request.method != "GET": + if request.method != 'GET': token = request.query_params.get("token", None) is_admin(token) def authenticate_header(self, request): - return "PermissionDenied" + return 'PermissionDenied' class Authenticator(BaseAuthentication): @@ -48,25 +47,26 @@ class Authenticator(BaseAuthentication): """ def authenticate(self, request): + token = request.query_params.get("token", None) obj = models.UserToken.objects.filter(token=token).first() if not obj: - raise exceptions.AuthenticationFailed( - {"code": "9998", "msg": "用户未认证", "success": False} - ) + raise exceptions.AuthenticationFailed({ + "code": "9998", + "msg": "用户未认证", + "success": False + }) update_time = int(obj.update_time.timestamp()) current_time = int(time.time()) if current_time - update_time >= INVALID_TIME: - raise exceptions.AuthenticationFailed( - { - "code": "9997", - "msg": "登陆超时,请重新登陆", - "success": False, - } - ) + raise exceptions.AuthenticationFailed({ + "code": "9997", + "msg": "登陆超时,请重新登陆", + "success": False + }) # valid update valid time obj.token = token @@ -75,7 +75,7 @@ def authenticate(self, request): return obj.user, obj def authenticate_header(self, request): - return "Auth Failed" + return 'Auth Failed' class DeleteAuthenticator(BaseAuthentication): @@ -84,29 +84,30 @@ class DeleteAuthenticator(BaseAuthentication): """ def authenticate(self, request): - if request.method == "DELETE": + if request.method == 'DELETE': token = request.query_params.get("token", None) is_admin(token) def authenticate_header(self, request): - return "PermissionDenied" + return 'PermissionDenied' class MyJWTAuthentication(JSONWebTokenAuthentication): + def authenticate(self, request): """ Returns a two-tuple of `User` and token if a valid signature has been supplied using JWT-based authentication. Otherwise returns `None`. """ # jwt_value = request.query_params.get("token", None) - jwt_value = request.headers.get("authorization", None) + jwt_value = request.headers.get('authorization', None) try: payload = jwt_decode_handler(jwt_value) except jwt.ExpiredSignature: - msg = "签名过期" + msg = '签名过期' raise exceptions.AuthenticationFailed(msg) except jwt.DecodeError: - msg = "签名解析失败" + msg = '签名解析失败' raise exceptions.AuthenticationFailed(msg) except jwt.InvalidTokenError: raise exceptions.AuthenticationFailed() @@ -123,17 +124,17 @@ def authenticate_credentials(self, payload): username = jwt_get_username_from_payload(payload) if not username: - msg = _("Invalid payload.") + msg = _('Invalid payload.') raise exceptions.AuthenticationFailed(msg) try: user = User.objects.get_by_natural_key(username) except User.DoesNotExist: - msg = "用户不存在" + msg = '用户不存在' raise exceptions.AuthenticationFailed(msg) if not user.is_active: - msg = "用户已禁用" + msg = '用户已禁用' raise exceptions.AuthenticationFailed(msg) return user diff --git a/FasterRunner/database.py b/FasterRunner/database.py index c38c0520..7b0372ba 100644 --- a/FasterRunner/database.py +++ b/FasterRunner/database.py @@ -1,7 +1,8 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: database.py +# @File: database.py # @Time : 2019/6/13 17:08 # @Email: lihuacai168@gmail.com # @Software: PyCharm @@ -10,16 +11,17 @@ class AutoChoiceDataBase: run_system = platform.system() - is_windows = run_system == "Windows" + is_windows = run_system == 'Windows' def db_for_read(self, model, **hints): if self.is_windows: - return "default" + return 'default' else: - return "remote" + return 'remote' def db_for_write(self, model, **hints): if self.is_windows: - return "default" + return 'default' else: - return "remote" + return 'remote' + diff --git a/FasterRunner/log.py b/FasterRunner/log.py index 82a5e4cd..f742b52c 100644 --- a/FasterRunner/log.py +++ b/FasterRunner/log.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: log.py @@ -7,11 +8,9 @@ import logging - class DatabaseLogHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: from system.models import LogRecord # 引入上面定义的LogRecord模型 - LogRecord.objects.create( request_id=record.request_id, level=record.levelname, diff --git a/FasterRunner/mycelery.py b/FasterRunner/mycelery.py index d3391d83..96dc921a 100644 --- a/FasterRunner/mycelery.py +++ b/FasterRunner/mycelery.py @@ -1,6 +1,5 @@ import logging import os - from celery import Celery from celery.signals import after_setup_logger diff --git a/FasterRunner/mycelery_dev.py b/FasterRunner/mycelery_dev.py index 936bf7d3..577791e7 100644 --- a/FasterRunner/mycelery_dev.py +++ b/FasterRunner/mycelery_dev.py @@ -1,15 +1,13 @@ import os - from celery import Celery - # set the default Django settings module for the 'celery' program. # from django.conf import settings from FasterRunner.settings import dev as settings # os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.dev") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings.dev') -app = Celery("FasterRunner") +app = Celery('FasterRunner') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. diff --git a/FasterRunner/pagination.py b/FasterRunner/pagination.py index 3237cee5..bd29d776 100644 --- a/FasterRunner/pagination.py +++ b/FasterRunner/pagination.py @@ -5,9 +5,8 @@ class MyCursorPagination(pagination.CursorPagination): """ Cursor 光标分页 性能高,安全 """ - page_size = 9 - ordering = "-update_time" + ordering = '-update_time' page_size_query_param = "pages" max_page_size = 20 @@ -16,8 +15,9 @@ class MyPageNumberPagination(pagination.PageNumberPagination): """ 普通分页,数据量越大性能越差 """ - page_size = 11 - page_size_query_param = "size" - page_query_param = "page" + page_size_query_param = 'size' + page_query_param = 'page' max_page_size = 20 + + diff --git a/FasterRunner/routers.py b/FasterRunner/routers.py index c38c0520..7b0372ba 100644 --- a/FasterRunner/routers.py +++ b/FasterRunner/routers.py @@ -1,7 +1,8 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: database.py +# @File: database.py # @Time : 2019/6/13 17:08 # @Email: lihuacai168@gmail.com # @Software: PyCharm @@ -10,16 +11,17 @@ class AutoChoiceDataBase: run_system = platform.system() - is_windows = run_system == "Windows" + is_windows = run_system == 'Windows' def db_for_read(self, model, **hints): if self.is_windows: - return "default" + return 'default' else: - return "remote" + return 'remote' def db_for_write(self, model, **hints): if self.is_windows: - return "default" + return 'default' else: - return "remote" + return 'remote' + diff --git a/FasterRunner/settings/__init__.py b/FasterRunner/settings/__init__.py index 933d9f5b..21398729 100644 --- a/FasterRunner/settings/__init__.py +++ b/FasterRunner/settings/__init__.py @@ -1,7 +1,8 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: __init__.py.py +# @File: __init__.py.py # @Time : 2019/6/14 17:05 # @Email: lihuacai168@gmail.com # @Software: PyCharm @@ -9,10 +10,8 @@ # 兼容已经移除的方法 import django from django.utils.encoding import smart_str - django.utils.encoding.smart_text = smart_str from django.utils.translation import gettext_lazy - django.utils.translation.ugettext = gettext_lazy -django.utils.translation.ugettext_lazy = gettext_lazy +django.utils.translation.ugettext_lazy = gettext_lazy \ No newline at end of file diff --git a/FasterRunner/settings/base.py b/FasterRunner/settings/base.py index 55223265..1adefcd8 100644 --- a/FasterRunner/settings/base.py +++ b/FasterRunner/settings/base.py @@ -57,7 +57,7 @@ "rest_framework_swagger", "drf_yasg", "system", - "django_auth_ldap", + "django_auth_ldap" ] MIDDLEWARE = [ @@ -77,13 +77,9 @@ # https://docs.djangoproject.com/en/2.1/howto/static-files/ STATIC_URL = "/static/" -STATICFILES_DIRS = [ - os.path.join(BASE_DIR, "static") -] # 指定static文件的路径,缺少这个配置,collect static 无法加载extent.js +STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")] # 指定static文件的路径,缺少这个配置,collect static 无法加载extent.js -STATIC_ROOT = os.path.join( - BASE_DIR, "static_root" -) # collect static 之后的文件存放路径 +STATIC_ROOT = os.path.join(BASE_DIR, "static_root") # collect static 之后的文件存放路径 ROOT_URLCONF = "FasterRunner.urls" @@ -250,7 +246,7 @@ "ERROR": "red", "CRITICAL": "bold_red", }, - }, + } # 日志格式 }, "filters": { @@ -291,10 +287,10 @@ "formatter": "color", "filters": ["request_id"], }, - "db": { - "level": "INFO", + 'db': { + 'level': 'INFO', "formatter": "standard", - "class": "FasterRunner.log.DatabaseLogHandler", # 指向你的自定义处理器 + 'class': 'FasterRunner.log.DatabaseLogHandler', # 指向你的自定义处理器 }, }, "loggers": { @@ -336,33 +332,34 @@ # 邮箱配置 EMAIL_USE_SSL = True -EMAIL_HOST = os.environ.get( - "EMAIL_HOST", "smtp.qq.com" -) # 如果是 163 改成 smtp.163.com +EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.qq.com") # 如果是 163 改成 smtp.163.com EMAIL_PORT = os.environ.get("EMAIL_PORT", 465) # 默认是qq邮箱端口 EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER") # 配置邮箱 EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD") # 对应的授权码 DEFAULT_FROM_EMAIL = EMAIL_HOST_USER + # LDAP配置 import ldap from django_auth_ldap.config import LDAPSearch -USE_LDAP = False # 如果需要开启LDAP认证,就设置位True +USE_LDAP = False # 如果需要开启LDAP认证,就设置位True if USE_LDAP: - AUTHENTICATION_BACKENDS = ("django_auth_ldap.backend.LDAPBackend",) + AUTHENTICATION_BACKENDS = ( + 'django_auth_ldap.backend.LDAPBackend', + ) -AUTH_LDAP_SERVER_URI = "ldap://localhost:389" # LDAP服务器地址,默认端口389 +AUTH_LDAP_SERVER_URI = "ldap://localhost:389" # LDAP服务器地址,默认端口389 -AUTH_LDAP_BIND_DN = "cn=admin,dc=myorg,dc=com" # LDAP管理员账号 -AUTH_LDAP_BIND_PASSWORD = "admin" # LDAP管理员密码 +AUTH_LDAP_BIND_DN = "cn=admin,dc=myorg,dc=com" # LDAP管理员账号 +AUTH_LDAP_BIND_PASSWORD = "admin" # LDAP管理员密码 AUTH_LDAP_USER_SEARCH = LDAPSearch( "ou=Tester,dc=myorg,dc=com", ldap.SCOPE_SUBTREE, "(uid=%(user)s)", -) # LDAP搜索账号,ou可以理解为组织单位或者部门,不填写也是ok,dc可以理解为域名 +) # LDAP搜索账号,ou可以理解为组织单位或者部门,不填写也是ok,dc可以理解为域名 AUTH_LDAP_USER_ATTR_MAP = { "username": "uid", @@ -372,4 +369,4 @@ } # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' diff --git a/FasterRunner/settings/docker.py b/FasterRunner/settings/docker.py index 01999665..9f9d3453 100644 --- a/FasterRunner/settings/docker.py +++ b/FasterRunner/settings/docker.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- from os import environ diff --git a/FasterRunner/settings/pro.py b/FasterRunner/settings/pro.py index fb5c0829..108b1af4 100644 --- a/FasterRunner/settings/pro.py +++ b/FasterRunner/settings/pro.py @@ -1,10 +1,10 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- +from dotenv import load_dotenv, find_dotenv from os import environ - -from dotenv import find_dotenv, load_dotenv - from .base import * +import os DEBUG = False @@ -12,21 +12,21 @@ if find_dotenv(): load_dotenv(find_dotenv()) # RabbitMQ 账号密码 - MQ_USER = environ.get("RABBITMQ_DEFAULT_USER") - MQ_PASSWORD = environ.get("RABBITMQ_DEFAULT_PASS") - MQ_HOST = environ.get("MQ_HOST") + MQ_USER = environ.get('RABBITMQ_DEFAULT_USER') + MQ_PASSWORD = environ.get('RABBITMQ_DEFAULT_PASS') + MQ_HOST = environ.get('MQ_HOST') - SERVER_IP = environ.get("SERVER_IP") + SERVER_IP = environ.get('SERVER_IP') # 数据库账号密码 - MYSQL_HOST = environ.get("MYSQL_HOST") - DB_NAME = environ.get("MYSQL_DATABASE") - DB_USER = environ.get("MYSQL_USER") - DB_PASSWORD = environ.get("MYSQL_PASSWORD") - PLATFORM_NAME = environ.get("PLATFORM_NAME") + MYSQL_HOST = environ.get('MYSQL_HOST') + DB_NAME = environ.get('MYSQL_DATABASE') + DB_USER = environ.get('MYSQL_USER') + DB_PASSWORD = environ.get('MYSQL_PASSWORD') + PLATFORM_NAME = environ.get('PLATFORM_NAME') if PLATFORM_NAME: - IM_REPORT_SETTING.update({"platform_name": PLATFORM_NAME}) + IM_REPORT_SETTING.update({'platform_name': PLATFORM_NAME}) - SENTRY_DSN = environ.get("SENTRY_DSN") + SENTRY_DSN = environ.get('SENTRY_DSN') if SENTRY_DSN: import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration @@ -34,33 +34,35 @@ sentry_sdk.init( dsn=SENTRY_DSN, integrations=[DjangoIntegration()], + # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. # We recommend adjusting this value in production. traces_sample_rate=1.0, + # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. - send_default_pii=True, + send_default_pii=True ) DATABASES = { - "default": { - "ENGINE": "django.db.backends.mysql", - "HOST": MYSQL_HOST, - "NAME": DB_NAME, # 新建数据库名 - "USER": DB_USER, # 数据库登录名 - "PASSWORD": DB_PASSWORD, # 数据库登录密码 - "OPTIONS": {"charset": "utf8mb4"}, - "TEST": { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'HOST': MYSQL_HOST, + 'NAME': DB_NAME, # 新建数据库名 + 'USER': DB_USER, # 数据库登录名 + 'PASSWORD': DB_PASSWORD, # 数据库登录密码 + 'OPTIONS': {'charset': 'utf8mb4'}, + 'TEST': { # 'MIRROR': 'default', # 单元测试时,使用default的配置 # 'DEPENDENCIES': ['default'] - }, + } } } -broker_url = f"amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:5672//" +broker_url = f'amqp://{MQ_USER}:{MQ_PASSWORD}@{MQ_HOST}:5672//' -BASE_REPORT_URL = f"http://{SERVER_IP}:8000/api/fastrunner/reports" +BASE_REPORT_URL = f'http://{SERVER_IP}:8000/api/fastrunner/reports' # 用来直接url访问 # STATIC_URL = '/static/' diff --git a/FasterRunner/swagger.py b/FasterRunner/swagger.py index 001d0df0..f18345d8 100644 --- a/FasterRunner/swagger.py +++ b/FasterRunner/swagger.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: swagger.py @@ -14,8 +15,8 @@ def get_tags(self, operation_keys=None): if "api" in tags and len(operation_keys) >= 3: # `operation_keys` 内容像这样 ['v1', 'prize_join_log', 'create'] - if operation_keys[2].startswith("run"): - tags[0] = "run" + if operation_keys[2].startswith('run'): + tags[0] = 'run' else: tags[0] = operation_keys[2] diff --git a/FasterRunner/urls.py b/FasterRunner/urls.py index 4d21dc35..44653d5d 100644 --- a/FasterRunner/urls.py +++ b/FasterRunner/urls.py @@ -14,20 +14,23 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import include, path, re_path -from drf_yasg import openapi -from drf_yasg.views import get_schema_view -from rest_framework import permissions +from django.urls import path, include, re_path from rest_framework.routers import DefaultRouter -from rest_framework_jwt.views import obtain_jwt_token + from fastrunner.views import run_all_auto_case from system import views as system_views + +from rest_framework import permissions +from drf_yasg.views import get_schema_view +from drf_yasg import openapi + +from rest_framework_jwt.views import obtain_jwt_token schema_view = get_schema_view( openapi.Info( title="Snippets API", - default_version="v1", + default_version='v1', description="Test description", terms_of_service="https://www.google.com/policies/terms/", contact=openapi.Contact(email="contact@snippets.local"), @@ -38,47 +41,26 @@ authentication_classes=[], ) system_router = DefaultRouter() -system_router.register(r"log_records", system_views.LogRecordViewSet) +system_router.register(r'log_records', system_views.LogRecordViewSet) urlpatterns = [ path(r"login", obtain_jwt_token), - path("admin/", admin.site.urls), + path('admin/', admin.site.urls), # re_path(r'^docs/', schema_view, name="docs"), - path( - "accounts/", - include( - "rest_framework.urls", - ), - ), - path( - "api-auth/", - include("rest_framework.urls", namespace="rest_framework_api_auth"), - ), - path("api/user/", include("fastuser.urls")), - path("api/fastrunner/", include("fastrunner.urls")), - path("api/system/", include(system_router.urls)), + path('accounts/', include('rest_framework.urls',)), + path('api-auth/', include('rest_framework.urls', namespace='rest_framework_api_auth')), + path('api/user/', include('fastuser.urls')), + path('api/fastrunner/', include('fastrunner.urls')), + path('api/system/', include(system_router.urls)), + # 执行定时任务 # TODO 需要增加触发检验,暂时关闭触发入口 # re_path(r'^run_all_auto_case/$', run_all_auto_case.run_all_auto_case, name='run_all_auto_case'), - path( - "get_report_url/", - run_all_auto_case.get_report_url, - name="get_report_url", - ), + path('get_report_url/', run_all_auto_case.get_report_url, name='get_report_url'), + # swagger - re_path( - r"^swagger(?P\.json|\.yaml)$", - schema_view.without_ui(cache_timeout=0), - name="schema-json", - ), - path( - "swagger/", - schema_view.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - path( - "redoc/", - schema_view.with_ui("redoc", cache_timeout=0), - name="schema-redoc", - ), + re_path(r'^swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), + path('swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path('redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), + ] diff --git a/FasterRunner/wsgi.py b/FasterRunner/wsgi.py index fd8467c0..2767a3f9 100644 --- a/FasterRunner/wsgi.py +++ b/FasterRunner/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.pro") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings.pro') application = get_wsgi_application() diff --git a/FasterRunner/wsgi_docker.py b/FasterRunner/wsgi_docker.py index 31de2494..c15226f0 100644 --- a/FasterRunner/wsgi_docker.py +++ b/FasterRunner/wsgi_docker.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings.docker") +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings.docker') application = get_wsgi_application() diff --git a/curd/__init__.py b/curd/__init__.py index 7ce4a90c..2332cad4 100644 --- a/curd/__init__.py +++ b/curd/__init__.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py diff --git a/curd/base_curd.py b/curd/base_curd.py index babff36b..54d3e3b9 100644 --- a/curd/base_curd.py +++ b/curd/base_curd.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: base_curd.py diff --git a/curd/crud_helper.py b/curd/crud_helper.py index ef4adfef..d4ce5e83 100644 --- a/curd/crud_helper.py +++ b/curd/crud_helper.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: crud_helper.py # @Time : 2022/9/4 17:59 @@ -24,11 +25,15 @@ def create(creator: str, model: BModel, payload: dict) -> int: logger.info(f"create {model.__name__} success, id: {obj.id}") -def get_or_create(model: BModel, filter_kwargs: dict, defaults: dict) -> tuple["obj", bool]: +def get_or_create( + model: BModel, filter_kwargs: dict, defaults: dict +) -> tuple["obj", bool]: """ :raises DoesNotExist """ - logger.info(f"input: get_or_create={model.__name__}, filter_kwargs={filter_kwargs}, defaults={defaults}") + logger.info( + f"input: get_or_create={model.__name__}, filter_kwargs={filter_kwargs}, defaults={defaults}" + ) obj, created = model.objects.get_or_create( defaults=defaults, **filter_kwargs, @@ -41,7 +46,9 @@ def update( updater: str, payload: dict, ) -> int: - logger.info(f"input: update model={obj.__class__.__name__}, id={obj.id}, payload={payload}") + logger.info( + f"input: update model={obj.__class__.__name__}, id={obj.id}, payload={payload}" + ) if updater: obj.updater = updater for attr, value in payload.items(): diff --git a/fastrunner/admin.py b/fastrunner/admin.py index 846f6b40..8c38f3f3 100644 --- a/fastrunner/admin.py +++ b/fastrunner/admin.py @@ -1 +1,3 @@ +from django.contrib import admin + # Register your models here. diff --git a/fastrunner/apps.py b/fastrunner/apps.py index 3330bc06..4405a79d 100644 --- a/fastrunner/apps.py +++ b/fastrunner/apps.py @@ -2,4 +2,4 @@ class FastrunnerConfig(AppConfig): - name = "fastrunner" + name = 'fastrunner' diff --git a/fastrunner/dto/__init__.py b/fastrunner/dto/__init__.py index 012a8f07..4f80b6d8 100644 --- a/fastrunner/dto/__init__.py +++ b/fastrunner/dto/__init__.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py diff --git a/fastrunner/dto/tree_dto.py b/fastrunner/dto/tree_dto.py index 7c9b7097..4ffca779 100644 --- a/fastrunner/dto/tree_dto.py +++ b/fastrunner/dto/tree_dto.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: tree_dto.py @@ -14,7 +15,7 @@ class TreeUniqueIn(BaseModel): class TreeUpdateIn(BaseModel): - tree: list[dict] = Field(alias="body") + tree: list[dict] = Field(alias='body') class TreeOut(BaseModel): diff --git a/fastrunner/models.py b/fastrunner/models.py index 5b088e40..543f74c4 100644 --- a/fastrunner/models.py +++ b/fastrunner/models.py @@ -2,6 +2,7 @@ from django.db import models from django_celery_beat.models import PeriodicTask + # Create your models here. from model_utils import Choices @@ -20,17 +21,19 @@ class Meta: name = models.CharField("项目名称", unique=True, null=False, max_length=100) desc = models.CharField("简要介绍", max_length=100, null=False) responsible = models.CharField("创建人", max_length=20, null=False) - yapi_base_url = models.CharField("yapi的openapi url", max_length=100, null=False, default="", blank=True) + yapi_base_url = models.CharField( + "yapi的openapi url", max_length=100, null=False, default="", blank=True + ) yapi_openapi_token = models.CharField( - "yapi openapi的token", - max_length=128, - null=False, - default="", - blank=True, + "yapi openapi的token", max_length=128, null=False, default="", blank=True ) # jira相关的 - jira_project_key = models.CharField("jira项目key", null=False, default="", max_length=30, blank=True) - jira_bearer_token = models.CharField("jira bearer_token", null=False, default="", max_length=45, blank=True) + jira_project_key = models.CharField( + "jira项目key", null=False, default="", max_length=30, blank=True + ) + jira_bearer_token = models.CharField( + "jira bearer_token", null=False, default="", max_length=45, blank=True + ) is_deleted = models.IntegerField("是否删除", null=True, default=0) @@ -44,7 +47,9 @@ class Meta: db_table = "debugtalk" code = models.TextField("python代码", default="# write you code", null=False) - project = models.OneToOneField(to=Project, on_delete=models.CASCADE, db_constraint=False) + project = models.OneToOneField( + to=Project, on_delete=models.CASCADE, db_constraint=False + ) class Config(BaseTable): @@ -123,7 +128,9 @@ class Meta: @property def tasks(self): - task_objs = PeriodicTask.objects.filter(description=self.project.id).values("id", "name", "args") + task_objs = PeriodicTask.objects.filter(description=self.project.id).values( + "id", "name", "args" + ) return filter(lambda task: self.id in eval(task.pop("args")), task_objs) @@ -200,7 +207,9 @@ class Meta: summary = models.TextField("报告基础信息", null=False) project = models.ForeignKey(Project, on_delete=models.CASCADE, db_constraint=False) ci_metadata = jsonfield.JSONField() - ci_project_id = models.IntegerField("gitlab的项目id", default=0, null=True, db_index=True) + ci_project_id = models.IntegerField( + "gitlab的项目id", default=0, null=True, db_index=True + ) ci_job_id = models.CharField( "gitlab的项目id", unique=True, @@ -222,7 +231,9 @@ class Meta: verbose_name = "测试报告详情" db_table = "report_detail" - report = models.OneToOneField(Report, on_delete=models.CASCADE, null=True, db_constraint=False) + report = models.OneToOneField( + Report, on_delete=models.CASCADE, null=True, db_constraint=False + ) summary_detail = models.TextField("报告详细信息") @@ -252,16 +263,19 @@ class Visit(models.Model): user = models.CharField(max_length=100, verbose_name="访问url的用户名", db_index=True) ip = models.CharField(max_length=20, verbose_name="用户的ip", db_index=True) - project = models.CharField(max_length=4, verbose_name="项目id", db_index=True, default=0) + project = models.CharField( + max_length=4, verbose_name="项目id", db_index=True, default=0 + ) url = models.CharField(max_length=255, verbose_name="被访问的url", db_index=True) path = models.CharField( - max_length=100, - verbose_name="被访问的接口路径", - default="", - db_index=True, + max_length=100, verbose_name="被访问的接口路径", default="", db_index=True + ) + request_params = models.CharField( + max_length=255, verbose_name="请求参数", default="", db_index=True + ) + request_method = models.CharField( + max_length=7, verbose_name="请求方法", choices=METHODS, db_index=True ) - request_params = models.CharField(max_length=255, verbose_name="请求参数", default="", db_index=True) - request_method = models.CharField(max_length=7, verbose_name="请求方法", choices=METHODS, db_index=True) request_body = models.TextField(verbose_name="请求体") create_time = models.DateTimeField("创建时间", auto_now_add=True, db_index=True) diff --git a/fastrunner/serializers.py b/fastrunner/serializers.py index e8be2d73..0f94ac71 100644 --- a/fastrunner/serializers.py +++ b/fastrunner/serializers.py @@ -1,15 +1,17 @@ -import datetime import json import logging from typing import Union +import datetime + import croniter from django.db.models import Q from django_celery_beat.models import PeriodicTask -from rest_framework import serializers +from rest_framework import serializers from fastrunner import models from fastrunner.utils.parser import Parse + from fastrunner.utils.tree import get_tree_relation_name logger = logging.getLogger(__name__) @@ -43,12 +45,20 @@ def get_api_cover_rate(self, obj): """ 接口覆盖率,百分比后去两位小数点 """ - apis = models.API.objects.filter(project_id=obj.id, delete=0).filter(~Q(tag=4)).values("url", "method") + apis = ( + models.API.objects.filter(project_id=obj.id, delete=0) + .filter(~Q(tag=4)) + .values("url", "method") + ) api_unique = {f'{api["url"]}_{api["method"]}' for api in apis} case_steps = ( - models.CaseStep.objects.filter(case__project_id=obj.id).filter(~Q(method="config")).values("url", "method") + models.CaseStep.objects.filter(case__project_id=obj.id) + .filter(~Q(method="config")) + .values("url", "method") ) - case_steps_unique = {f'{case_step["url"]}_{case_step["method"]}' for case_step in case_steps} + case_steps_unique = { + f'{case_step["url"]}_{case_step["method"]}' for case_step in case_steps + } if len(api_unique) == 0: return "0.00" if len(case_steps_unique) > len(api_unique): @@ -159,14 +169,18 @@ def get_body(self, obj): class CIReportSerializer(serializers.Serializer): - ci_job_id = serializers.IntegerField(required=True, min_value=1, help_text="gitlab-ci job id") + ci_job_id = serializers.IntegerField( + required=True, min_value=1, help_text="gitlab-ci job id" + ) class CISerializer(serializers.Serializer): # project = serializers.IntegerField( # required=True, min_value=1, help_text='测试平台中某个项目的id') # task_ids = serializers.CharField(required=True, max_length=200, allow_blank=True) - ci_job_id = serializers.IntegerField(required=True, min_value=1, help_text="gitlab-ci job id") + ci_job_id = serializers.IntegerField( + required=True, min_value=1, help_text="gitlab-ci job id" + ) ci_job_url = serializers.CharField(required=True, max_length=500) ci_pipeline_id = serializers.IntegerField(required=True) ci_pipeline_url = serializers.CharField(required=True, max_length=500) @@ -174,7 +188,9 @@ class CISerializer(serializers.Serializer): ci_project_name = serializers.CharField(required=True, max_length=100) ci_project_namespace = serializers.CharField(required=True, max_length=100) env = serializers.CharField(required=False, max_length=100) - start_job_user = serializers.CharField(required=True, max_length=100, help_text="GITLAB_USER_NAME") + start_job_user = serializers.CharField( + required=True, max_length=100, help_text="GITLAB_USER_NAME" + ) class APIRelatedCaseSerializer(serializers.Serializer): @@ -382,9 +398,11 @@ def get_kwargs(self, obj): def get_args(self, obj): case_id_list = json.loads(obj.args) # 数据格式,list of dict : [{"id":case_id,"name":case_name}] - return list(models.Case.objects.filter(pk__in=case_id_list).values("id", "name")) + return list( + models.Case.objects.filter(pk__in=case_id_list).values("id", "name") + ) - def get_last_run_at(self, obj) -> str | int: + def get_last_run_at(self, obj) -> Union[str, int]: if obj.last_run_at: return int(obj.last_run_at.timestamp()) return "" @@ -397,10 +415,7 @@ class ScheduleDeSerializer(serializers.Serializer): switch = serializers.BooleanField(required=True, help_text="定时任务开关") crontab = serializers.CharField( - required=True, - help_text="定时任务表达式", - max_length=100, - allow_blank=True, + required=True, help_text="定时任务表达式", max_length=100, allow_blank=True ) ci_project_ids = serializers.CharField( required=True, @@ -409,16 +424,10 @@ class ScheduleDeSerializer(serializers.Serializer): ) strategy = serializers.CharField(required=True, help_text="发送通知策略", max_length=20) receiver = serializers.CharField( - required=True, - help_text="邮件接收者,暂时用不上", - allow_blank=True, - max_length=100, + required=True, help_text="邮件接收者,暂时用不上", allow_blank=True, max_length=100 ) mail_cc = serializers.CharField( - required=True, - help_text="邮件抄送列表,暂时用不上", - allow_blank=True, - max_length=100, + required=True, help_text="邮件抄送列表,暂时用不上", allow_blank=True, max_length=100 ) name = serializers.CharField(required=True, help_text="定时任务的名字", max_length=100) webhook = serializers.CharField( @@ -443,14 +452,20 @@ class ScheduleDeSerializer(serializers.Serializer): allow_blank=True, ) data = serializers.ListField(required=True, help_text="用例id") - project = serializers.IntegerField(required=True, help_text="测试平台的项目id", min_value=1) + project = serializers.IntegerField( + required=True, help_text="测试平台的项目id", min_value=1 + ) def validate_ci_project_ids(self, ci_project_ids): if ci_project_ids: not_allowed_project_ids = set() - kwargs_list = PeriodicTask.objects.filter(~Q(description=self.initial_data["project"])).values("kwargs") + kwargs_list = PeriodicTask.objects.filter( + ~Q(description=self.initial_data["project"]) + ).values("kwargs") for kwargs in kwargs_list: - not_allowed_project_id: str = json.loads(kwargs["kwargs"]).get("ci_project_ids", "") + not_allowed_project_id: str = json.loads(kwargs["kwargs"]).get( + "ci_project_ids", "" + ) if not_allowed_project_id: not_allowed_project_ids.update(not_allowed_project_id.split(",")) @@ -460,4 +475,6 @@ def validate_ci_project_ids(self, ci_project_ids): validation_errors.add(ci_project_id) if validation_errors: - raise serializers.ValidationError(f"{','.join(validation_errors)} 已经在其他项目存在") + raise serializers.ValidationError( + f"{','.join(validation_errors)} 已经在其他项目存在" + ) diff --git a/fastrunner/services/__init__.py b/fastrunner/services/__init__.py index 1913ec9f..a7fd7ca1 100644 --- a/fastrunner/services/__init__.py +++ b/fastrunner/services/__init__.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py diff --git a/fastrunner/services/tree_service_impl.py b/fastrunner/services/tree_service_impl.py index 4b9e6570..d890b7e9 100644 --- a/fastrunner/services/tree_service_impl.py +++ b/fastrunner/services/tree_service_impl.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: tree_service_impl.py # @Time : 2022/9/4 18:57 @@ -12,11 +13,8 @@ from curd.base_curd import GenericCURD from fastrunner.dto.tree_dto import TreeOut, TreeUniqueIn, TreeUpdateIn from fastrunner.models import Relation -from fastrunner.utils.response import ( - TREE_ADD_SUCCESS, - TREE_UPDATE_SUCCESS, - StandResponse, -) +from fastrunner.utils.response import (TREE_ADD_SUCCESS, TREE_UPDATE_SUCCESS, + StandResponse) from fastrunner.utils.tree import get_tree_max_id @@ -45,10 +43,12 @@ def get_or_create(self, query: TreeUniqueIn) -> StandResponse[TreeOut]: } return StandResponse[TreeOut](**TREE_ADD_SUCCESS, data=TreeOut(**tree)) - def patch(self, pk: int, payload: TreeUpdateIn) -> StandResponse[TreeOut | None]: + def patch( + self, pk: int, payload: TreeUpdateIn + ) -> StandResponse[Union[TreeOut, None]]: try: pk: int = self.curd.update_obj_by_pk(pk, None, payload.dict()) - except Exception: + except Exception as e: err: str = traceback.format_exc() logger.warning(f"update tree {err=}") return StandResponse[None](code="9999", success=False, msg=err, data=None) diff --git a/fastrunner/tasks.py b/fastrunner/tasks.py index 5ebb43b6..d840a885 100644 --- a/fastrunner/tasks.py +++ b/fastrunner/tasks.py @@ -1,23 +1,25 @@ +import datetime import logging -from celery import Task, shared_task -from django.core.exceptions import ObjectDoesNotExist +from celery import shared_task, Task from django_celery_beat.models import PeriodicTask - +from django.core.exceptions import ObjectDoesNotExist from fastrunner import models -from fastrunner.utils import lark_message +from fastrunner.utils.loader import save_summary, debug_suite, debug_api from fastrunner.utils.ding_message import DingMessage -from fastrunner.utils.loader import debug_api, debug_suite, save_summary +from fastrunner.utils import lark_message -log = logging.getLogger(__name__) +log = logging.getLogger(__name__) def update_task_total_run_count(task_id): if task_id: task = PeriodicTask.objects.get(id=task_id) total_run_count = task.total_run_count + 1 dt = task.date_changed - PeriodicTask.objects.filter(id=task_id).update(date_changed=dt, total_run_count=total_run_count) + PeriodicTask.objects.filter(id=task_id).update( + date_changed=dt, total_run_count=total_run_count + ) class MyBaseTask(Task): @@ -62,10 +64,16 @@ def schedule_debug_suite(*args, **kwargs): override_config = kwargs.get("config", "") override_config_body = None if override_config and override_config != "请选择": - override_config_body = eval(models.Config.objects.get(name=override_config, project__id=project).body) + override_config_body = eval( + models.Config.objects.get(name=override_config, project__id=project).body + ) for content in suite: - test_list = models.CaseStep.objects.filter(case__id=content["id"]).order_by("step").values("body") + test_list = ( + models.CaseStep.objects.filter(case__id=content["id"]) + .order_by("step") + .values("body") + ) testcase_list = [] config = None @@ -75,7 +83,11 @@ def schedule_debug_suite(*args, **kwargs): if override_config_body: config = override_config_body continue - config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) + config = eval( + models.Config.objects.get( + name=body["name"], project__id=project + ).body + ) continue testcase_list.append(body) config_list.append(config) @@ -83,12 +95,7 @@ def schedule_debug_suite(*args, **kwargs): is_parallel = kwargs.get("is_parallel", False) summary, _ = debug_suite( - test_sets, - project, - suite, - config_list, - save=False, - allow_parallel=is_parallel, + test_sets, project, suite, config_list, save=False, allow_parallel=is_parallel ) task_name = kwargs["task_name"] @@ -99,11 +106,7 @@ def schedule_debug_suite(*args, **kwargs): report_type = 3 report_id = save_summary( - task_name, - summary, - project, - type=report_type, - user=kwargs.get("user", ""), + task_name, summary, project, type=report_type, user=kwargs.get("user", "") ) strategy = kwargs["strategy"] @@ -113,6 +116,7 @@ def schedule_debug_suite(*args, **kwargs): DING_OPEN_API: str = "https://oapi.dingtalk.com" FEISHU_OPEN_API: str = "https://open.feishu.cn" + if webhook.startswith(DING_OPEN_API) is False and webhook.startswith(FEISHU_OPEN_API) is False: log.warning(f"{webhook=}还不支持, 目前仅支持钉钉和飞书") @@ -127,5 +131,8 @@ def schedule_debug_suite(*args, **kwargs): log.info("开始发送飞书消息") summary["task_name"] = task_name summary["report_id"] = report_id - lark_message.send_message(summary=summary, webhook=webhook, case_count=len(args)) + lark_message.send_message( + summary=summary, webhook=webhook, case_count=len(args) + ) log.info("发送飞书消息完成") + diff --git a/fastrunner/templatetags/custom_tags.py b/fastrunner/templatetags/custom_tags.py index e5674bd3..b0078c8a 100644 --- a/fastrunner/templatetags/custom_tags.py +++ b/fastrunner/templatetags/custom_tags.py @@ -6,24 +6,17 @@ register = template.Library() -@register.filter(name="json_dumps") +@register.filter(name='json_dumps') def json_dumps(value): try: - return json.dumps( - json.loads(value), - indent=4, - separators=(",", ": "), - ensure_ascii=False, - ) + return json.dumps(json.loads(value), indent=4, separators=(',', ': '), ensure_ascii=False) except Exception: return value -@register.filter(name="convert_timestamp") +@register.filter(name='convert_timestamp') def convert_timestamp(value): try: - return time.strftime( - "%Y--%m--%d %H:%M:%S", time.localtime(int(float(value))) - ) + return time.strftime("%Y--%m--%d %H:%M:%S", time.localtime(int(float(value)))) except: - return value + return value \ No newline at end of file diff --git a/fastrunner/test.py b/fastrunner/test.py index 1b2d133b..ad1a34e9 100644 --- a/fastrunner/test.py +++ b/fastrunner/test.py @@ -1,17 +1,17 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: test.py +# @File: test.py # @Time : 2019/6/13 11:53 # @Email: lihuacai168@gmail.com # @Software: PyCharm from django.test import TestCase - from fastuser import models - class ModelTest(TestCase): + def setUp(self): """ 注册:{ @@ -20,12 +20,8 @@ def setUp(self): "email": "1@1.com" } """ - models.UserInfo.objects.create( - username="rikasai", - password="mypassword", - email="lihuacai168@gmail.com", - ) + models.UserInfo.objects.create(username='rikasai', password='mypassword', email='lihuacai168@gmail.com') def test_user_register(self): - res = models.UserInfo.objects.get(username="rikasai") + res = models.UserInfo.objects.get(username='rikasai') self.assertEqual(res.email, "lihuacai168@gmail.com") diff --git a/fastrunner/tests/__init__.py b/fastrunner/tests/__init__.py index 59598a69..15b8ad8d 100644 --- a/fastrunner/tests/__init__.py +++ b/fastrunner/tests/__init__.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py diff --git a/fastrunner/tests/test_tree_service_impl.py b/fastrunner/tests/test_tree_service_impl.py index 88c65ca5..80dcee05 100644 --- a/fastrunner/tests/test_tree_service_impl.py +++ b/fastrunner/tests/test_tree_service_impl.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: test_tree_service_impl.py # @Time : 2022/9/5 00:22 @@ -16,11 +17,15 @@ class TestTreeServiceImpl(TestCase): service = tree_service def test_get_or_create(self): - assert self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree == self.default_tree + assert ( + self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree + == self.default_tree + ) assert ( # cover exist case - self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree == self.default_tree + self.service.get_or_create(TreeUniqueIn(project_id=100, type=1)).data.tree + == self.default_tree ) def test_patch(self): @@ -31,15 +36,19 @@ def test_patch(self): "children": [{"id": 2, "label": "sub", "children": []}], } ] - pk: int = self.service.get_or_create(TreeUniqueIn(project_id=101, type=1)).data.id + pk: int = self.service.get_or_create( + TreeUniqueIn(project_id=101, type=1) + ).data.id self.service.patch(pk=pk, payload=TreeUpdateIn(body=input_body)) - assert self.service.get_or_create(TreeUniqueIn(project_id=101, type=1)).data.tree == input_body + assert ( + self.service.get_or_create(TreeUniqueIn(project_id=101, type=1)).data.tree + == input_body + ) assert ( self.service.patch( - pk=999, - payload=TreeUpdateIn(body=input_body), # pk not exist + pk=999, payload=TreeUpdateIn(body=input_body) # pk not exist ).data is None ) diff --git a/fastrunner/urls.py b/fastrunner/urls.py index 8bb903ec..6c5dcf69 100644 --- a/fastrunner/urls.py +++ b/fastrunner/urls.py @@ -15,192 +15,161 @@ """ from django.urls import path - -from fastrunner.views import ( - api, - ci, - config, - project, - report, - run, - schedule, - suite, - yapi, -) +from fastrunner.views import project, api, config, schedule, run, suite, report, yapi, ci urlpatterns = [ # 访问统计相关接口 - path( - "visit/", - project.VisitView.as_view( - { - "get": "list", - } - ), - ), - path( - "gitlab-ci/", - ci.CIView.as_view({"post": "run_ci_tests", "get": "get_ci_report_url"}), - ), + path('visit/', project.VisitView.as_view({ + "get": "list", + })), + + path('gitlab-ci/', ci.CIView.as_view({'post': 'run_ci_tests', 'get': 'get_ci_report_url'})), + # 项目相关接口地址 - path( - "project/", - project.ProjectView.as_view( - { - "get": "list", - "post": "add", - "patch": "update", - "delete": "delete", - } - ), - ), - path("project//", project.ProjectView.as_view({"get": "single"})), - path( - "project/yapi//", - project.ProjectView.as_view({"get": "yapi_info"}), - ), - path("dashboard/", project.DashBoardView.as_view({"get": "get"})), + path('project/', project.ProjectView.as_view({ + "get": "list", + "post": "add", + "patch": "update", + "delete": "delete" + })), + path('project//', project.ProjectView.as_view({"get": "single"})), + path('project/yapi//', project.ProjectView.as_view({"get": "yapi_info"})), + + path('dashboard/', project.DashBoardView.as_view({'get': 'get'})), + # 定时任务相关接口 - path( - "schedule/", - schedule.ScheduleView.as_view( - { - "get": "list", - "post": "add", - } - ), - ), - path( - "schedule//", - schedule.ScheduleView.as_view({"get": "run", "delete": "delete", "put": "update", "post": "copy"}), - ), + path('schedule/', schedule.ScheduleView.as_view({ + "get": "list", + "post": "add", + })), + + path('schedule//', schedule.ScheduleView.as_view({ + "get": "run", + "delete": "delete", + "put": "update", + "post": "copy" + })), + # debugtalk.py相关接口地址 - path( - "debugtalk//", - project.DebugTalkView.as_view({"get": "debugtalk"}), - ), - path( - "debugtalk/", - project.DebugTalkView.as_view({"patch": "update", "post": "run"}), - ), + path('debugtalk//', project.DebugTalkView.as_view({"get": "debugtalk"})), + path('debugtalk/', project.DebugTalkView.as_view({ + "patch": "update", + "post": "run" + })), + # 二叉树接口地址 - path("tree//", project.TreeView.as_view()), + path('tree//', project.TreeView.as_view()), + # 导入yapi - path("yapi//", yapi.YAPIView.as_view()), + path('yapi//', yapi.YAPIView.as_view()), + # 文件上传 修改 删除接口地址 # path('file/', project.FileView.as_view()), + # api接口模板地址 - path("api/", api.APITemplateView.as_view({"post": "add", "get": "list"})), - path( - "api//", - api.APITemplateView.as_view( - { - "delete": "delete", - "get": "single", - "patch": "update", - "post": "copy", - } - ), - ), - path( - "api/tag/", - api.APITemplateView.as_view( - { - "patch": "add_tag", # api修改状态 - } - ), - ), - path( - "api/move_api/", - api.APITemplateView.as_view( - { - "patch": "move", # api修改relation - } - ), - ), - path( - "test/move_case/", - suite.TestCaseView.as_view( - { - "patch": "move", # case修改relation - } - ), - ), - path( - "test/tag/", - suite.TestCaseView.as_view( - { - "patch": "update_tag", # case修改tag - } - ), - ), - path( - "api/sync//", - api.APITemplateView.as_view( - { - "patch": "sync_case", # api同步用例步骤 - } - ), - ), + path('api/', api.APITemplateView.as_view({ + "post": "add", + "get": "list" + })), + + path('api//', api.APITemplateView.as_view({ + "delete": "delete", + "get": "single", + "patch": "update", + "post": "copy" + })), + + path('api/tag/', api.APITemplateView.as_view({ + "patch": "add_tag", # api修改状态 + })), + + path('api/move_api/', api.APITemplateView.as_view({ + "patch": "move", # api修改relation + })), + + path('test/move_case/', suite.TestCaseView.as_view({ + "patch": "move", # case修改relation + })), + + path('test/tag/', suite.TestCaseView.as_view({ + "patch": "update_tag", # case修改tag + })), + + path('api/sync//', api.APITemplateView.as_view({ + "patch": "sync_case", # api同步用例步骤 + })), + # test接口地址 - path( - "test/", - suite.TestCaseView.as_view({"get": "get", "post": "post", "delete": "delete"}), - ), - path( - "test//", - suite.TestCaseView.as_view( - { - "delete": "delete", - "post": "copy", - "put": "put", # 请求方法和处理方法同名时可以省略 - } - ), - ), - path("teststep//", suite.CaseStepView.as_view()), + path('test/', suite.TestCaseView.as_view({ + "get": "get", + "post": "post", + "delete": "delete" + })), + + path('test//', suite.TestCaseView.as_view({ + "delete": "delete", + "post": "copy", + "put": "put" # 请求方法和处理方法同名时可以省略 + + })), + + path('teststep//', suite.CaseStepView.as_view()), + # config接口地址 - path( - "config/", - config.ConfigView.as_view({"post": "add", "get": "list", "delete": "delete"}), - ), - path( - "config//", - config.ConfigView.as_view( - { - "post": "copy", - "delete": "delete", - "patch": "update", - "get": "all", - } - ), - ), - path( - "variables/", - config.VariablesView.as_view({"post": "add", "get": "list", "delete": "delete"}), - ), - path( - "variables//", - config.VariablesView.as_view({"delete": "delete", "patch": "update"}), - ), + path('config/', config.ConfigView.as_view({ + "post": "add", + "get": "list", + "delete": "delete" + })), + + path('config//', config.ConfigView.as_view({ + "post": "copy", + "delete": "delete", + "patch": "update", + "get": "all" + })), + + path('variables/', config.VariablesView.as_view({ + "post": "add", + "get": "list", + "delete": "delete" + })), + + path('variables//', config.VariablesView.as_view({ + "delete": "delete", + "patch": "update" + })), + # run api - path("run_api_pk//", run.run_api_pk), - path("run_api_tree/", run.run_api_tree), - path("run_api/", run.run_api), + path('run_api_pk//', run.run_api_pk), + path('run_api_tree/', run.run_api_tree), + path('run_api/', run.run_api), + # run testsuite - path("run_testsuite/", run.run_testsuite), - path("run_test/", run.run_test), - path("run_testsuite_pk//", run.run_testsuite_pk), - path("run_suite_tree/", run.run_suite_tree), - path("run_multi_tests/", run.run_multi_tests), + path('run_testsuite/', run.run_testsuite), + path('run_test/', run.run_test), + path('run_testsuite_pk//', run.run_testsuite_pk), + path('run_suite_tree/', run.run_suite_tree), + path('run_multi_tests/', run.run_multi_tests), + # 报告地址 - path("reports/", report.ReportView.as_view({"get": "list"})), - path( - "reports//", - report.ReportView.as_view({"delete": "delete", "get": "look"}), - ), - path("host_ip/", config.HostIPView.as_view({"post": "add", "get": "list"})), - path( - "host_ip//", - config.HostIPView.as_view({"delete": "delete", "patch": "update", "get": "all"}), - ), + path('reports/', report.ReportView.as_view({ + "get": "list" + })), + + path('reports//', report.ReportView.as_view({ + "delete": "delete", + "get": "look" + })), + + path('host_ip/', config.HostIPView.as_view({ + "post": "add", + "get": "list" + })), + + path('host_ip//', config.HostIPView.as_view({ + "delete": "delete", + "patch": "update", + "get": "all" + })), ] diff --git a/fastrunner/utils/convert2boomer.py b/fastrunner/utils/convert2boomer.py index acaafbae..20172ce8 100644 --- a/fastrunner/utils/convert2boomer.py +++ b/fastrunner/utils/convert2boomer.py @@ -1,9 +1,8 @@ import json -from enum import Enum from typing import Optional from pydantic import BaseModel, conint - +from enum import Enum from fastrunner.utils.convert2hrp import Hrp @@ -15,16 +14,16 @@ class JsonValueType(str, Enum): class BoomerExtendCmd(BaseModel): - max_rps: int | None - master_host: str = "10.129.144.24" + max_rps: Optional[int] + master_host: str = '10.129.144.24' master_port: int = 5557 json_value_type: str = JsonValueType.interface.value replace_str_index: dict disable_keepalive: conint(gt=0, lt=1) = 0 - cpu_profile: str | None - cpu_profile_duration: int | None - mem_profile: str | None - mem_profile_duration: int | None + cpu_profile: Optional[str] + cpu_profile_duration: Optional[int] + mem_profile: Optional[str] + mem_profile_duration: Optional[int] class BoomerIn(BaseModel): @@ -33,54 +32,54 @@ class BoomerIn(BaseModel): verbose: conint(gt=0, lt=1) = 0 -class Boomer: +class Boomer(object): def __init__(self, hrp: Hrp, extend_cmd: BoomerExtendCmd): self.hrp = hrp self.extend_cmd = extend_cmd def to_boomer_cmd(self, verbose: bool = True) -> str: - data_path: str = "/home/toc/SDE/code/locust-boomer/data.csv" - image = "boomer:latest" - end = " \\\n" - base_cmd = f"docker run -v {data_path}:/app/data.csv {image}{end}" - req_cmd = "" + data_path: str = '/home/toc/SDE/code/locust-boomer/data.csv' + image = 'boomer:latest' + end = ' \\\n' + base_cmd = f'docker run -v {data_path}:/app/data.csv {image}{end}' + req_cmd = '' for k, v in self.hrp.get_request().dict().items(): if isinstance(v, (dict, list)): - v = f"'{json.dumps(v, indent=4)}'" + v = f'\'{json.dumps(v, indent=4)}\'' if isinstance(v, bool): v = 1 if v else 0 if isinstance(v, (str, int)): - v = f"{v}{end}" + v = f'{v}{end}' - if k == "url": - req_cmd += f"--{k}={v}" - if k == "method": - req_cmd += f"--{k}={v}" - if k == "headers": - req_cmd += f"--json-headers={v}" - if k == "req_json": - req_cmd += f"--raw-data={v}" - cmd = f"{base_cmd}{req_cmd}" + if k == 'url': + req_cmd += f'--{k}={v}' + if k == 'method': + req_cmd += f'--{k}={v}' + if k == 'headers': + req_cmd += f'--json-headers={v}' + if k == 'req_json': + req_cmd += f'--raw-data={v}' + cmd = f'{base_cmd}{req_cmd}' for k, v in self.extend_cmd.dict().items(): if isinstance(v, (str, int)): - v = f"{v}{end}" - if k == "master_host": - cmd += f"--master-host={v}" - if k == "master_port": - cmd += f"--master-port={v}" - if k == "disable-keepalive" and v: - cmd += "--disable-keepalive 1" - if k == "max_rps" and v is not None: - cmd += f"--max-rps={v}" - if k == "json_value_type": - cmd += f"--json-value-type={v}" - if k == "replace_str_index": - cmd += f"--replace-str-index='{json.dumps(v, indent=4)}'{end}" + v = f'{v}{end}' + if k == 'master_host': + cmd += f'--master-host={v}' + if k == 'master_port': + cmd += f'--master-port={v}' + if k == 'disable-keepalive' and v: + cmd += f'--disable-keepalive 1' + if k == 'max_rps' and v is not None: + cmd += f'--max-rps={v}' + if k == 'json_value_type': + cmd += f'--json-value-type={v}' + if k == 'replace_str_index': + cmd += f'--replace-str-index=\'{json.dumps(v, indent=4)}\'{end}' if verbose: - cmd += "--verbose 1" - if cmd[-1] == "\\": + cmd += '--verbose 1' + if cmd[-1] == '\\': cmd = cmd[:-1] return cmd diff --git a/fastrunner/utils/convert2hrp.py b/fastrunner/utils/convert2hrp.py index 9cfdc095..a8525744 100644 --- a/fastrunner/utils/convert2hrp.py +++ b/fastrunner/utils/convert2hrp.py @@ -1,27 +1,29 @@ import json import os -from collections.abc import Callable from enum import Enum -from typing import Any, Dict, List, Text, Union +from typing import Any +from typing import Dict, Text, Union, Callable +from typing import List from urllib.parse import urlparse -from pydantic import BaseModel, Field, HttpUrl +from pydantic import BaseModel, Field +from pydantic import HttpUrl -Name = str -Url = str -BaseUrl = Union[HttpUrl, str] -VariablesMapping = dict[str, Any] -FunctionsMapping = dict[str, Callable] -Headers = dict[str, str] -Cookies = dict[str, str] +Name = Text +Url = Text +BaseUrl = Union[HttpUrl, Text] +VariablesMapping = Dict[Text, Any] +FunctionsMapping = Dict[Text, Callable] +Headers = Dict[Text, Text] +Cookies = Dict[Text, Text] Verify = bool -Hooks = list[str | dict[str, str]] -Export = list[str] -Validators = list[dict] -Env = dict[str, Any] +Hooks = List[Union[Text, Dict[Text, Text]]] +Export = List[Text] +Validators = List[Dict] +Env = Dict[Text, Any] -class MethodEnum(str, Enum): +class MethodEnum(Text, Enum): GET = "GET" POST = "POST" PUT = "PUT" @@ -31,18 +33,17 @@ class MethodEnum(str, Enum): PATCH = "PATCH" NA = "N/A" - class TConfig(BaseModel): name: Name verify: Verify = False base_url: BaseUrl = "" # Text: prepare variables in debugtalk.py, ${gen_variables()} - variables: VariablesMapping | str = {} - parameters: VariablesMapping | str = {} + variables: Union[VariablesMapping, Text] = {} + parameters: Union[VariablesMapping, Text] = {} # setup_hooks: Hooks = [] # teardown_hooks: Hooks = [] export: Export = [] - path: str = None + path: Text = None weight: int = 1 @@ -51,21 +52,21 @@ class TRequest(BaseModel): method: MethodEnum url: Url - params: dict[str, str] = {} + params: Dict[Text, Text] = {} headers: Headers = {} - req_json: dict | list | str = Field(None) - body: str | dict[str, Any] = None + req_json: Union[Dict, List, Text] = Field(None) + body: Union[Text, Dict[Text, Any]] = None cookies: Cookies = {} timeout: float = 120 allow_redirects: bool = True verify: Verify = False - upload: dict = {} # used for upload files + upload: Dict = {} # used for upload files class TStep(BaseModel): name: Name - request: TRequest | None = None - testcase: str | Callable | None = None + request: Union[TRequest, None] = None + testcase: Union[Text, Callable, None] = None variables: VariablesMapping = {} setup_hooks: Hooks = [] teardown_hooks: Hooks = [] @@ -74,37 +75,37 @@ class TStep(BaseModel): # used to export session variables from referenced testcase export: Export = [] validators: Validators = Field([], alias="validate") - validate_script: list[str] = [] + validate_script: List[Text] = [] class TestCase(BaseModel): config: TConfig - teststeps: list[TStep] + teststeps: List[TStep] class ProjectMeta(BaseModel): - debugtalk_py: str = "" # debugtalk.py file content - debugtalk_path: str = "" # debugtalk.py file path - dot_env_path: str = "" # .env file path + debugtalk_py: Text = "" # debugtalk.py file content + debugtalk_path: Text = "" # debugtalk.py file path + dot_env_path: Text = "" # .env file path functions: FunctionsMapping = {} # functions defined in debugtalk.py env: Env = {} - RootDir: str = os.getcwd() # project root directory (ensure absolute), the path debugtalk.py located + RootDir: Text = os.getcwd() # project root directory (ensure absolute), the path debugtalk.py located class TestsMapping(BaseModel): project_meta: ProjectMeta - testcases: list[TestCase] + testcases: List[TestCase] class TestCaseTime(BaseModel): start_at: float = 0 - start_at_iso_format: str = "" + start_at_iso_format: Text = "" duration: float = 0 class TestCaseInOut(BaseModel): config_vars: VariablesMapping = {} - export_vars: dict = {} + export_vars: Dict = {} class RequestStat(BaseModel): @@ -114,9 +115,9 @@ class RequestStat(BaseModel): class AddressData(BaseModel): - client_ip: str = "N/A" + client_ip: Text = "N/A" client_port: int = 0 - server_ip: str = "N/A" + server_ip: Text = "N/A" server_port: int = 0 @@ -125,16 +126,16 @@ class RequestData(BaseModel): url: Url headers: Headers = {} cookies: Cookies = {} - body: str | bytes | list | dict | None = {} + body: Union[Text, bytes, List, Dict, None] = {} class ResponseData(BaseModel): status_code: int - headers: dict + headers: Dict cookies: Cookies - encoding: str | None = None - content_type: str - body: str | bytes | list | dict + encoding: Union[Text, None] = None + content_type: Text + body: Union[Text, bytes, List, Dict] class ReqRespData(BaseModel): @@ -148,18 +149,18 @@ class SessionData(BaseModel): success: bool = False # in most cases, req_resps only contains one request & response # while when 30X redirect occurs, req_resps will contain multiple request & response - req_resps: list[ReqRespData] = [] + req_resps: List[ReqRespData] = [] stat: RequestStat = RequestStat() address: AddressData = AddressData() - validators: dict = {} + validators: Dict = {} class StepData(BaseModel): """teststep data, each step maybe corresponding to one request or one testcase""" success: bool = False - name: str = "" # teststep name - data: SessionData | list["StepData"] = None + name: Text = "" # teststep name + data: Union[SessionData, List['StepData']] = None export_vars: VariablesMapping = {} @@ -167,31 +168,31 @@ class StepData(BaseModel): class TestCaseSummary(BaseModel): - name: str + name: Text success: bool - case_id: str + case_id: Text time: TestCaseTime in_out: TestCaseInOut = {} - log: str = "" - step_datas: list[StepData] = [] + log: Text = "" + step_datas: List[StepData] = [] class PlatformInfo(BaseModel): - httprunner_version: str - python_version: str - platform: str + httprunner_version: Text + python_version: Text + platform: Text class TestCaseRef(BaseModel): - name: str - base_url: str = "" - testcase: str + name: Text + base_url: Text = "" + testcase: Text variables: VariablesMapping = {} class TestSuite(BaseModel): config: TConfig - testcases: list[TestCaseRef] + testcases: List[TestCaseRef] class Stat(BaseModel): @@ -205,35 +206,35 @@ class TestSuiteSummary(BaseModel): stat: Stat = Stat() time: TestCaseTime = TestCaseTime() platform: PlatformInfo - testcases: list[TestCaseSummary] + testcases: List[TestCaseSummary] -class Hrp: +class Hrp(object): def __init__(self, faster_req_json: dict): self.faster_req_json = faster_req_json def parse_url(self): - url = self.faster_req_json["url"] + url = self.faster_req_json['url'] o = urlparse(url=url) - baseurl = o.scheme + "://" + o.netloc + baseurl = o.scheme + '://' + o.netloc return baseurl, o.path def get_headers(self): - headers: dict = self.faster_req_json.get("headers", {}) + headers: dict = self.faster_req_json.get('headers', {}) # Content-Length may be error - headers.pop("Content-Length", None) + headers.pop('Content-Length', None) return headers def get_request(self) -> TRequest: base_url, path = self.parse_url() req = TRequest( - method=self.faster_req_json["method"], + method=self.faster_req_json['method'], url=base_url + path, - params=self.faster_req_json.get("params", {}), + params=self.faster_req_json.get('params', {}), headers=self.get_headers(), - body=self.faster_req_json.get("body", {}), - req_json=self.faster_req_json.get("json", {}), - verify=self.faster_req_json.get("verify", False), + body=self.faster_req_json.get('body', {}), + req_json=self.faster_req_json.get('json', {}), + verify=self.faster_req_json.get('verify', False), ) return req @@ -260,7 +261,7 @@ def get_testcase(self) -> TestCase: ) -source_json = """ +source_json = ''' { "url": "http://10.129.144.22:8081/post", "method": "POST", @@ -317,8 +318,8 @@ def get_testcase(self) -> TestCase: ] } } -""" +''' -if __name__ == "__main__": +if __name__ == '__main__': hrp = Hrp(json.loads(source_json)) print(hrp.get_testcase().json()) diff --git a/fastrunner/utils/day.py b/fastrunner/utils/day.py index 157b5e52..9b05a459 100644 --- a/fastrunner/utils/day.py +++ b/fastrunner/utils/day.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: day.py @@ -22,7 +23,7 @@ def get_day(days: int = 0, **kwargs): """ d = datetime.timedelta(days) n = datetime.datetime.now() - time_str = "%Y-%m-%d" + time_str = f"%Y-%m-%d" if kwargs: h = kwargs.get("h", "00") m = kwargs.get("m", "00") @@ -82,12 +83,12 @@ def get_month(month_delta: int = 0, base_ts=get_ts_int): month_delta = -(abs(month_delta) % 12) # 月份差是正数,有两种情况 - # 1.月份差和当前的月份相加 > 12, 年份+1, 月份等于超过的减去12 - # 2.月份差和当前月份相加 <= 12, 直接相加 + # 1.月份差和当前的月份相加 > 12, 年份+1, 月份等于超过的减去12 + # 2.月份差和当前月份相加 <= 12, 直接相加 if month_delta >= 0: if month + month_delta > 12: year += 1 - month = month + month_delta - 12 + month = (month + month_delta - 12) else: month += month_delta else: @@ -100,7 +101,7 @@ def get_month(month_delta: int = 0, base_ts=get_ts_int): month = 12 + (month + month_delta) else: month += month_delta - month = str(month).rjust(2, "0") + month = str(month).rjust(2, '0') return f"{year}{month}" @@ -115,7 +116,7 @@ def get_week_format(weeks): return f"{week[:4]}年{week[4:]}周" -if __name__ == "__main__": +if __name__ == '__main__': import doctest doctest.testmod(verbose=True) diff --git a/fastrunner/utils/decorator.py b/fastrunner/utils/decorator.py index aadb6125..b645e411 100644 --- a/fastrunner/utils/decorator.py +++ b/fastrunner/utils/decorator.py @@ -2,6 +2,7 @@ import logging # from loguru import logger + from fastrunner.utils import parser logger = logging.getLogger(__name__) @@ -11,7 +12,9 @@ def request_log(level): def wrapper(func): @functools.wraps(func) def inner_wrapper(request, *args, **kwargs): - msg_data = "before process request data:\n{data}".format(data=parser.format_json(request.data)) + msg_data = "before process request data:\n{data}".format( + data=parser.format_json(request.data) + ) msg_params = "before process request params:\n{params}".format( params=parser.format_json(request.query_params) ) diff --git a/fastrunner/utils/ding_message.py b/fastrunner/utils/ding_message.py index ba391a1f..bf00e1d9 100644 --- a/fastrunner/utils/ding_message.py +++ b/fastrunner/utils/ding_message.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 # @File: ding_message.py @@ -7,7 +8,6 @@ # @Software: PyCharm import logging - # import os # os.environ.setdefault( # "DJANGO_SETTINGS_MODULE", "FasterRunner.settings.dev" @@ -16,13 +16,13 @@ # import django # # django.setup() + from dingtalkchatbot.chatbot import DingtalkChatbot from fastrunner.utils.parser import format_summary_to_ding log = logging.getLogger(__name__) - class DingMessage: """ 调用钉钉机器人发送测试结果 @@ -33,7 +33,9 @@ def __init__(self, run_type: str = "auto", webhook: str = ""): self.robot = DingtalkChatbot(webhook) def send_ding_msg(self, summary, report_name=None): - msg_and_fail_count = format_summary_to_ding("markdown", summary, report_name=report_name) + msg_and_fail_count = format_summary_to_ding( + "markdown", summary, report_name=report_name + ) msg = msg_and_fail_count[0] fail_count = msg_and_fail_count[1] title = "FasterRunner自动化测试报告" @@ -54,7 +56,9 @@ def send_ding_msg(self, summary, report_name=None): for phone in [f"@{phone} " for phone in receive_msg_mobiles]: at_phone += phone msg += at_phone - self.robot.send_markdown(title=title, text=msg, at_mobiles=receive_msg_mobiles) + self.robot.send_markdown( + title=title, text=msg, at_mobiles=receive_msg_mobiles + ) if __name__ == "__main__": diff --git a/fastrunner/utils/host.py b/fastrunner/utils/host.py index 2317bb06..d7b57bc3 100644 --- a/fastrunner/utils/host.py +++ b/fastrunner/utils/host.py @@ -1,8 +1,9 @@ -import re from urllib.parse import urlparse +import re def parse_host(ip, api): + if not isinstance(ip, list): return api if not api: @@ -13,15 +14,12 @@ def parse_host(ip, api): parts = urlparse(api["request"]["base_url"]) # 返回值是Host:port host = parts.netloc - host = host.split(":")[0] + host = host.split(':')[0] if host: for content in ip: content = content.strip() if host in content and not content.startswith("#"): - ip = re.findall( - r"\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b", - content, - ) + ip = re.findall(r'\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b', content) # ip = re.findall(r'\b(?:25[0-5]\.|2[0-4]\d\.|[01]?\d\d?\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b:\d{0,5}', content) if ip: if "headers" in api["request"].keys(): diff --git a/fastrunner/utils/lark_message.py b/fastrunner/utils/lark_message.py index 4b80679c..be5b1682 100644 --- a/fastrunner/utils/lark_message.py +++ b/fastrunner/utils/lark_message.py @@ -1,94 +1,114 @@ +# -*- coding: utf-8 -*- # @Time : 2020/8/11 16:12 # @Author : lihuacai # @Email : lihuacai168@gmail.com # @File : lark_message.py # @Software: PyCharm -import json -import logging - import requests +import json # from loguru import logger + +from fastrunner import models from django.conf import settings +from fastrunner.utils.loader import back_async + +import logging logger = logging.getLogger(__name__) def get_base_post_content(): return { "msg_type": "post", - "content": {"post": {"zh_cn": {"title": "", "content": []}}}, + "content": { + "post": { + "zh_cn": { + "title": "", + "content": [ + ] + } + } + } } def parse_message(summary: dict, msg_type: str, **kwargs): task_name = summary["task_name"] - rows_count = summary["stat"]["testsRun"] - pass_count = summary["stat"]["successes"] - fail_count = summary["stat"]["failures"] - error_count = summary["stat"]["errors"] - duration = "%.2fs" % summary["time"]["duration"] - report_id = summary["report_id"] - base_url = settings.IM_REPORT_SETTING.get("base_url") - port = settings.IM_REPORT_SETTING.get("port") - report_url = f"{base_url}:{port}/api/fastrunner/reports/{report_id}/" + rows_count = summary['stat']['testsRun'] + pass_count = summary['stat']['successes'] + fail_count = summary['stat']['failures'] + error_count = summary['stat']['errors'] + duration = '%.2fs' % summary['time']['duration'] + report_id = summary['report_id'] + base_url = settings.IM_REPORT_SETTING.get('base_url') + port = settings.IM_REPORT_SETTING.get('port') + report_url = f'{base_url}:{port}/api/fastrunner/reports/{report_id}/' executed = rows_count - fail_rate = f"{fail_count / executed:.2%}" + fail_rate = '{:.2%}'.format(fail_count / executed) # 富文本 - if msg_type == "post": + if msg_type == 'post': msg_template = get_base_post_content() - content = [ - [{"text": f"任务名称:{task_name}"}], - [{"text": f"总共耗时: {duration}"}], - [{"text": f"成功接口: {pass_count}"}], - [{"text": f"异常接口: {error_count}"}], - [{"text": f"失败接口: {fail_count}"}], - [{"text": f"失败比例: {fail_rate}"}], - [{"text": f"查看详情: {report_url}"}], - ] - ci_job_url: str = kwargs.get("ci_job_url") + content = [[{'text': f'任务名称:{task_name}'}], + [{'text': f'总共耗时: {duration}'}], + [{'text': f'成功接口: {pass_count}'}], + [{'text': f'异常接口: {error_count}'}], + [{'text': f'失败接口: {fail_count}'}], + [{'text': f'失败比例: {fail_rate}'}], + [{'text': f'查看详情: {report_url}'}]] + ci_job_url: str = kwargs.get('ci_job_url') if ci_job_url: - content.append([{"text": f"ci_job: {ci_job_url}"}]) - ci_pipeline_url: str = kwargs.get("ci_pipeline_url") + content.append([{'text': f'ci_job: {ci_job_url}'}]) + ci_pipeline_url: str = kwargs.get('ci_pipeline_url') if ci_pipeline_url: - content.append([{"text": f"ci_pipeline: {ci_pipeline_url}"}]) + content.append([{'text': f'ci_pipeline: {ci_pipeline_url}'}]) - case_count = kwargs.get("case_count") + case_count = kwargs.get('case_count') if case_count: - content.insert(2, [{"text": f"用例个数: {case_count}"}]) + content.insert(2, [{'text': f'用例个数: {case_count}'}]) for d in content: - d[0].update({"tag": "text"}) - msg_template["content"]["post"]["zh_cn"]["content"] = content + d[0].update({'tag': 'text'}) + msg_template['content']['post']['zh_cn']['content'] = content return msg_template text = f" 任务名称: {task_name}\n 总共耗时: {duration}\n 成功接口: {pass_count}个\n 异常接口: {error_count}个\n 失败接口: {fail_count}个\n 失败比例: {fail_rate}\n 查看详情: {report_url}" return text def send_message(summary: dict, webhook: str, **kwargs): - """ """ + """ + + """ # v1 https://open.feishu.cn/open-apis/bot/hook/xxx # v2 https://open.feishu.cn/open-apis/bot/v2/hook/xxx - title = settings.IM_REPORT_SETTING.get("report_title") - platform_name = settings.IM_REPORT_SETTING.get("platform_name", "FasterRunner测试平台") + title = settings.IM_REPORT_SETTING.get('report_title') + platform_name = settings.IM_REPORT_SETTING.get('platform_name', 'FasterRunner测试平台') if platform_name: title = platform_name + title - webhooks = webhook.split("\n") + webhooks = webhook.split('\n') for webhook in webhooks: version = webhook[37:39] - if version == "v2": - msg = parse_message(summary=summary, msg_type="post", **kwargs) - msg["content"]["post"]["zh_cn"]["title"] = title + if version == 'v2': + msg = parse_message(summary=summary, msg_type='post', **kwargs) + msg['content']['post']['zh_cn']['title'] = title data = msg else: msg = parse_message(summary=summary, msg_type=None) - data = {"title": title, "text": msg} - if "seatalk" in webhook: + data = { + "title": title, + "text": msg + } + if 'seatalk' in webhook: msg = parse_message(summary=summary, msg_type=None) - data = {"tag": "text", "text": {"content": msg}} + data = { + "tag": "text", + "text": { + "content": msg + } + } res = requests.post(url=webhook, data=json.dumps(data).encode("utf-8")).json() - if res.get("StatusCode") == 0: + if res.get('StatusCode') == 0: logger.info(f"发送通知成功,请求的webhook是: {webhook}") else: logger.error(f"发送通知失败,请求的webhook是: {webhook}, 响应是:{res}") diff --git a/fastrunner/utils/loader.py b/fastrunner/utils/loader.py index f3b799bd..4854dfa2 100644 --- a/fastrunner/utils/loader.py +++ b/fastrunner/utils/loader.py @@ -18,9 +18,9 @@ import requests import yaml from bs4 import BeautifulSoup - # from loguru import logger from requests.cookies import RequestsCookieJar +from requests_toolbelt import MultipartEncoder from FasterRunner.settings.base import BASE_DIR from fastrunner import models @@ -32,11 +32,7 @@ logger = logging.getLogger(__name__) -TEST_NOT_EXISTS = { - "code": "0102", - "status": False, - "msg": "节点下没有接口或者用例集", -} +TEST_NOT_EXISTS = {"code": "0102", "status": False, "msg": "节点下没有接口或者用例集"} def is_function(tup): @@ -63,11 +59,11 @@ def is_variable(tup): return True -class FileLoader: +class FileLoader(object): @staticmethod def dump_yaml_file(yaml_file, data): """dump yaml file""" - with open(yaml_file, "w", encoding="utf-8") as stream: + with io.open(yaml_file, "w", encoding="utf-8") as stream: yaml.dump( data, stream, @@ -80,25 +76,21 @@ def dump_yaml_file(yaml_file, data): @staticmethod def dump_json_file(json_file, data): """dump json file""" - with open(json_file, "w", encoding="utf-8") as stream: + with io.open(json_file, "w", encoding="utf-8") as stream: json.dump( - data, - stream, - indent=4, - separators=(",", ": "), - ensure_ascii=False, + data, stream, indent=4, separators=(",", ": "), ensure_ascii=False ) @staticmethod def dump_python_file(python_file, data): """dump python file""" - with open(python_file, "w", encoding="utf-8") as stream: + with io.open(python_file, "w", encoding="utf-8") as stream: stream.write(data) @staticmethod def dump_binary_file(binary_file, data): """dump file""" - with open(binary_file, "wb") as stream: + with io.open(binary_file, "wb") as stream: stream.write(data) @staticmethod @@ -142,10 +134,7 @@ def load_python_module(file_path): def parse_validate_and_extract( - list_of_dict: list, - variables_mapping: dict, - functions_mapping, - api_variables: list, + list_of_dict: list, variables_mapping: dict, functions_mapping, api_variables: list ): """ Args: @@ -168,6 +157,7 @@ def parse_validate_and_extract( # validate: d是{'equals': ['v1', 'v2']}, v类型是list v = list(d.values())[0] try: + # validate,extract 的值包含了api variable的key中,不需要替换 for key in api_variables_key: if isinstance(v, str): @@ -202,12 +192,7 @@ def parse_tests(testcases, debugtalk, name=None, config=None, project=None): config: none or dict debugtalk: dict """ - refs = { - "env": {}, - "def-api": {}, - "def-testcase": {}, - "debugtalk": debugtalk, - } + refs = {"env": {}, "def-api": {}, "def-testcase": {}, "debugtalk": debugtalk} testset = { "config": {"name": testcases[-1]["name"], "variables": []}, @@ -221,8 +206,12 @@ def parse_tests(testcases, debugtalk, name=None, config=None, project=None): testset["config"]["name"] = name # 获取当前项目的全局变量 - global_variables = models.Variables.objects.filter(project=project).all().values("key", "value") - all_config_variables_keys = set().union(*(d.keys() for d in testset["config"].setdefault("variables", []))) + global_variables = ( + models.Variables.objects.filter(project=project).all().values("key", "value") + ) + all_config_variables_keys = set().union( + *(d.keys() for d in testset["config"].setdefault("variables", [])) + ) global_variables_list_of_dict = [] for item in global_variables: if item["key"] not in all_config_variables_keys: @@ -247,8 +236,12 @@ def parse_tests(testcases, debugtalk, name=None, config=None, project=None): extract: list = testcase.get("extract", []) validate: list = testcase.get("validate", []) api_variables: list = testcase.get("variables", []) - parse_validate_and_extract(extract, variables_mapping, functions_mapping, api_variables) - parse_validate_and_extract(validate, variables_mapping, functions_mapping, api_variables) + parse_validate_and_extract( + extract, variables_mapping, functions_mapping, api_variables + ) + parse_validate_and_extract( + validate, variables_mapping, functions_mapping, api_variables + ) return testset @@ -261,7 +254,9 @@ def load_debugtalk(project): code = models.Debugtalk.objects.get(project__id=project).code # file_path = os.path.join(tempfile.mkdtemp(prefix='FasterRunner'), "debugtalk.py") - tempfile_path = tempfile.mkdtemp(prefix="FasterRunner", dir=os.path.join(BASE_DIR, "tempWorkDir")) + tempfile_path = tempfile.mkdtemp( + prefix="FasterRunner", dir=os.path.join(BASE_DIR, "tempWorkDir") + ) file_path = os.path.join(tempfile_path, "debugtalk.py") os.chdir(tempfile_path) try: @@ -269,7 +264,7 @@ def load_debugtalk(project): debugtalk = FileLoader.load_python_module(os.path.dirname(file_path)) return debugtalk, file_path - except Exception: + except Exception as e: os.chdir(BASE_DIR) shutil.rmtree(os.path.dirname(file_path)) @@ -312,7 +307,9 @@ def run_test(test_set: dict): workers = min(len(test_sets), 10) with ThreadPoolExecutor(max_workers=workers) as executor: futures = {executor.submit(run_test, t): t for t in test_sets} - results = [future.result() for future in concurrent.futures.as_completed(futures)] + results = [ + future.result() for future in concurrent.futures.as_completed(futures) + ] duration = time.time() - start return merge_parallel_result(results, duration) @@ -378,7 +375,9 @@ def debug_suite( failure_case_config.update(obj[index]) failure_case_config_mapping_list.append(failure_case_config) case_count = len(test_sets) - case_fail_rate = "{:.2%}".format(len(failure_case_config_mapping_list) / case_count) + case_fail_rate = "{:.2%}".format( + len(failure_case_config_mapping_list) / case_count + ) summary["stat"].update( { "failure_case_config_mapping_list": failure_case_config_mapping_list, @@ -453,11 +452,7 @@ def debug_api(api, project, name=None, config=None, save=True, user=""): # testcase_list = [parse_tests(api, load_debugtalk(project), name=name, config=config)] testcase_list = [ parse_tests( - api, - debugtalk_content, - name=name, - config=config, - project=project, + api, debugtalk_content, name=name, config=config, project=project ) ] @@ -477,7 +472,9 @@ def debug_api(api, project, name=None, config=None, save=True, user=""): json_data = record["meta_data"]["response"].pop("json", {}) if json_data: record["meta_data"]["response"]["jsonCopy"] = json_data - ConvertRequest.generate_curl(summary["details"], convert_type=("curl", "boomer")) + ConvertRequest.generate_curl( + summary["details"], convert_type=("curl", "boomer") + ) return summary except Exception as e: logger.error(f"debug_api error: {e}") @@ -500,12 +497,16 @@ def load_test(test, project=None): except KeyError: if "case" in test.keys(): if test["body"]["method"] == "config": - case_step = models.Config.objects.get(name=test["body"]["name"], project=project) + case_step = models.Config.objects.get( + name=test["body"]["name"], project=project + ) else: case_step = models.CaseStep.objects.get(id=test["id"]) else: if test["body"]["method"] == "config": - case_step = models.Config.objects.get(name=test["body"]["name"], project=project) + case_step = models.Config.objects.get( + name=test["body"]["name"], project=project + ) else: case_step = models.API.objects.get(id=test["id"]) @@ -532,23 +533,28 @@ def wrapper(*args, **kwargs): def parse_summary(summary): """序列化summary""" for detail in summary["details"]: + for record in detail["records"]: + for key, value in record["meta_data"]["request"].items(): if isinstance(value, bytes): record["meta_data"]["request"][key] = value.decode("utf-8") if isinstance(value, RequestsCookieJar): - record["meta_data"]["request"][key] = requests.utils.dict_from_cookiejar(value) + record["meta_data"]["request"][ + key + ] = requests.utils.dict_from_cookiejar(value) for key, value in record["meta_data"]["response"].items(): if isinstance(value, bytes): record["meta_data"]["response"][key] = value.decode("utf-8") if isinstance(value, RequestsCookieJar): - record["meta_data"]["response"][key] = requests.utils.dict_from_cookiejar(value) + record["meta_data"]["response"][ + key + ] = requests.utils.dict_from_cookiejar(value) if "text/html" in record["meta_data"]["response"]["content_type"]: record["meta_data"]["response"]["content"] = BeautifulSoup( - record["meta_data"]["response"]["content"], - features="html.parser", + record["meta_data"]["response"]["content"], features="html.parser" ).prettify() return summary diff --git a/fastrunner/utils/middleware.py b/fastrunner/utils/middleware.py index de7c8021..8b00fae0 100644 --- a/fastrunner/utils/middleware.py +++ b/fastrunner/utils/middleware.py @@ -18,6 +18,7 @@ def process_request(self, request): request._body = request.body def process_response(self, request, response): + body = request._body if body == b"": body = "" @@ -30,9 +31,7 @@ def process_response(self, request, response): else: user = request.user - ip: str = request.headers.get( - "x-forwarded-for", request.META.get("REMOTE_ADDR") - ) + ip: str = request.headers.get("x-forwarded-for", request.META.get("REMOTE_ADDR")) # 前端请求头没传project,就默认为0 project = request.headers.get("project", 0) @@ -55,7 +54,7 @@ def process_response(self, request, response): url=url, request_method=request.method, request_body=body, - ip=ip.split(",")[0], # 有时候会有多个ip,取第一个 + ip=ip.split(',')[0], # 有时候会有多个ip,取第一个 path=request.path, request_params=query_params[1:-1], project=project, @@ -97,7 +96,7 @@ def process_exception(self, request, exception): ): try: email_helper.send_mail( - subject="测试平台异常告警", + subject=f"测试平台异常告警", html_message=self.build_html_message( request=request, exception=exception ), @@ -105,10 +104,10 @@ def process_exception(self, request, exception): recipient_list=[settings.EMAIL_HOST_USER], ) except smtplib.SMTPAuthenticationError as e: - logger.error(f"邮件发送失败 {traceback.format_exception(e)}") + logger.error(f'邮件发送失败 {traceback.format_exception(e)}') return except Exception: - logger.error(f"邮件发送失败 {traceback.format_exc()}") + logger.error(f'邮件发送失败 {traceback.format_exc()}') return logger.info("邮件发送成功") diff --git a/fastrunner/utils/parser.py b/fastrunner/utils/parser.py index cd7d2590..3505dcc6 100644 --- a/fastrunner/utils/parser.py +++ b/fastrunner/utils/parser.py @@ -11,11 +11,8 @@ from loguru import logger from fastrunner import models -from fastrunner.utils.tree import ( - get_all_ycatid, - get_tree_max_id, - get_tree_ycatid_mapping, -) +from fastrunner.utils.tree import (get_all_ycatid, get_tree_max_id, + get_tree_ycatid_mapping) logger = logging.getLogger(__name__) @@ -34,12 +31,12 @@ class FileType(Enum): file = 7 -class Format: +class Format(object): """ 解析标准HttpRunner脚本 前端->后端 """ - def __init__(self, body: dict[str, str | dict], level: str = "test"): + def __init__(self, body: Dict[str, Union[str, Dict]], level: str = "test"): """ body => { header: header -> [{key:'', value:'', desc:''},], @@ -62,7 +59,9 @@ def __init__(self, body: dict[str, str | dict], level: str = "test"): self.__headers = body.get("header", {}).pop("header", None) if level == "test": - self.__params = body.get("request", {}).get("params", {}).pop("params", None) + self.__params = ( + body.get("request", {}).get("params", {}).pop("params", None) + ) self.__data = body.get("request", {}).get("form", {}).pop("data", None) self.__json = body.get("request", {}).pop("json", None) self.__files = body.get("request", {}).get("files", {}).pop("files", None) @@ -112,7 +111,7 @@ def __init__(self, body: dict[str, str | dict], level: str = "test"): self.rig_id = body.get("rig_id", None) self.rig_env = body.get("rig_env", 0) - def parse(self) -> dict[str, str | dict] | None: + def parse(self) -> Optional[Dict[str, Union[str, Dict]]]: """ 返回标准化HttpRunner "desc" 字段运行需去除 """ @@ -121,11 +120,7 @@ def parse(self) -> dict[str, str | dict] | None: "name": self.name, "rig_id": self.rig_id, "times": self.__times, - "request": { - "url": self.url, - "method": self.method, - "verify": False, - }, + "request": {"url": self.url, "method": self.method, "verify": False}, "desc": self.__desc, } @@ -166,7 +161,7 @@ def parse(self) -> dict[str, str | dict] | None: self.testcase = test -class Parse: +class Parse(object): """ 标准HttpRunner脚本解析至前端 后端->前端 """ @@ -243,16 +238,14 @@ def parse_test_level(self, test): test["times"] = self.__times test["method"] = self.__request["method"] test["url"] = self.__request["url"] - test["validate"] = [{"expect": "", "actual": "", "comparator": "equals", "type": 1}] + test["validate"] = [ + {"expect": "", "actual": "", "comparator": "equals", "type": 1} + ] test["extract"] = [{"key": "", "value": "", "desc": ""}] if self.__extract: test["extract"] = [ - { - "key": key, - "value": value, - "desc": self.__desc["extract"][key], - } + {"key": key, "value": value, "desc": self.__desc["extract"][key]} for content in self.__extract for key, value in content.items() ] @@ -291,11 +284,7 @@ def parse_config_level(self, test): def parse_request(self, test): if self.__request.get("headers"): test["header"] = [ - { - "key": key, - "value": value, - "desc": self.__desc["header"][key], - } + {"key": key, "value": value, "desc": self.__desc["header"][key]} for key, value in self.__request.pop("headers").items() ] @@ -347,21 +336,23 @@ def parse_hooks(self, test): test["hooks"] = [] if len(self.__setup_hooks) > len(self.__teardown_hooks): for index in range(0, len(self.__setup_hooks)): - teardown = self.__teardown_hooks[index] if index < len(self.__teardown_hooks) else "" + teardown = ( + self.__teardown_hooks[index] + if index < len(self.__teardown_hooks) + else "" + ) test["hooks"].append( - { - "setup": self.__setup_hooks[index], - "teardown": teardown, - } + {"setup": self.__setup_hooks[index], "teardown": teardown} ) else: for index in range(0, len(self.__teardown_hooks)): - setup = self.__setup_hooks[index] if index < len(self.__setup_hooks) else "" + setup = ( + self.__setup_hooks[index] + if index < len(self.__setup_hooks) + else "" + ) test["hooks"].append( - { - "setup": setup, - "teardown": self.__teardown_hooks[index], - } + {"setup": setup, "teardown": self.__teardown_hooks[index]} ) @@ -400,7 +391,9 @@ def format_summary_to_ding(msg_type, summary, report_name=None): # celery执行的报告名 if report_name: case_suite_name = report_name - start_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(summary["time"]["start_at"])) + start_at = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(summary["time"]["start_at"]) + ) duration = "%.2fs" % summary["time"]["duration"] # 已执行的条数 @@ -409,10 +402,10 @@ def format_summary_to_ding(msg_type, summary, report_name=None): env_name, base_url, start_at, duration, case_suite_name ) # 通过率 - pass_rate = f"{pass_count / executed:.2%}" + pass_rate = "{:.2%}".format(pass_count / executed) # 失败率 - fail_rate = f"{fail_count / executed:.2%}" + fail_rate = "{:.2%}".format(fail_count / executed) fail_count_list = [] @@ -429,8 +422,12 @@ def format_summary_to_ding(msg_type, summary, report_name=None): if record["status"] != "failure": continue else: - response_message = record["meta_data"]["response"]["json"]["info"]["message"] - response_error = record["meta_data"]["response"]["json"]["info"]["error"] + response_message = record["meta_data"]["response"]["json"]["info"][ + "message" + ] + response_error = record["meta_data"]["response"]["json"]["info"][ + "error" + ] request_url = record["meta_data"]["request"]["url"] case_name = record["name"] expect = [] @@ -460,9 +457,7 @@ def format_summary_to_ding(msg_type, summary, report_name=None): port: str = os.getenv("DJANGO_API_PORT", "8000") report_url = f"http://{ip}:{port}/api/fastrunner/reports/{report_id}/" for item in fail_count_list: - case_name_and_fail_message = ( - f'> - **{item["case_name"]} - {item["request_url"]} - {item["fail_message"]}**\n' - ) + case_name_and_fail_message = f'> - **{item["case_name"]} - {item["request_url"]} - {item["fail_message"]}**\n' fail_detail_markdown += case_name_and_fail_message msg_markdown = f""" ## FasterRunner自动化测试报告 @@ -476,11 +471,21 @@ def format_summary_to_ding(msg_type, summary, report_name=None): ### [查看详情]({report_url})""" else: - msg = f"""{title} - 总用例{rows_count}共条,执行了{executed}条,异常{error_count}条. - 通过{pass_count}条,通过率{pass_rate}. - 失败{fail_count}条,失败率{fail_rate}. - {fail_detail}""" + msg = """{0} + 总用例{1}共条,执行了{2}条,异常{3}条. + 通过{4}条,通过率{5}. + 失败{6}条,失败率{7}. + {8}""".format( + title, + rows_count, + executed, + error_count, + pass_count, + pass_rate, + fail_count, + fail_rate, + fail_detail, + ) return (msg_markdown, fail_count) if msg_markdown else (msg, fail_count) @@ -502,13 +507,19 @@ def set_customized_variable(api_info_template, items): } for attr in attribute_name_enum: api_info_template["variables"]["variables"].append({attr: ""}) - api_info_template["variables"]["desc"][attr] = attr_name.get("description", "") + api_info_template["variables"]["desc"][attr] = attr_name.get( + "description", "" + ) # 查询条件比较类型 range_type: dict = properties.get("rangeType", {}) range_type_enum: list = range_type.get("enum", [""]) - api_info_template["variables"]["variables"].append({"rangeType": range_type_enum[0]}) - api_info_template["variables"]["desc"]["rangeType"] = f'条件匹配方式: {",".join(range_type_enum)}' + api_info_template["variables"]["variables"].append( + {"rangeType": range_type_enum[0]} + ) + api_info_template["variables"]["desc"][ + "rangeType" + ] = f'条件匹配方式: {",".join(range_type_enum)}' # 默认排序 api_info_template["request"]["json"]["orderBy"] = [ @@ -533,7 +544,9 @@ def __init__(self, yapi_base_url: str, token: str, faster_project_id: int): def get_category_info(self): try: - res = requests.get(self.category_info_url, params={"token": self.__token}).json() + res = requests.get( + self.category_info_url, params={"token": self.__token} + ).json() except Exception as e: logger.error(f"获取yapi的目录失败: {e}") finally: @@ -725,7 +738,9 @@ def get_batch_api_detail(self, api_ids): def handle_request(api_id): try: - response = requests.get(f"{self.api_detail_url}?token={token}&id={api_id}") + response = requests.get( + f"{self.api_detail_url}?token={token}&id={api_id}" + ) res = response.json() api_info.append(res["data"]) except Exception as e: @@ -801,7 +816,9 @@ def yapi2faster(self, source_api_info): # 限制api的名称最大长度,避免溢出 api_info_template["name"] = source_api_info.get("title", "默认api名称")[:100] # path中{var}替换成$var格式 - api_info_template["url"] = source_api_info.get("path", "").replace("{", "$").replace("}", "") + api_info_template["url"] = ( + source_api_info.get("path", "").replace("{", "$").replace("}", "") + ) api_info_template["method"] = source_api_info.get("method", "GET") # yapi的分组id @@ -830,10 +847,7 @@ def yapi2faster(self, source_api_info): if isinstance(req_body, dict): req_body_properties = req_body.get("properties") if isinstance(req_body_properties, dict): - for ( - field_name, - field_value, - ) in req_body_properties.items(): + for field_name, field_value in req_body_properties.items(): if isinstance(field_value, dict) is False: continue @@ -844,7 +858,9 @@ def yapi2faster(self, source_api_info): field_type = field_value.get("type", "unKnow") if field_type == "unKnow": - logger.error(f'yapi: {source_api_info["_id"]}, req_body json type is unKnow') + logger.error( + f'yapi: {source_api_info["_id"]}, req_body json type is unKnow' + ) if not (field_type == "array" or field_type == "object"): self.set_ordinary_variable( @@ -876,7 +892,10 @@ def yapi2faster(self, source_api_info): property_value, ) in properties.items(): field_type = property_value["type"] - if not (field_type == "array" or field_type == "object"): + if not ( + field_type == "array" + or field_type == "object" + ): self.set_ordinary_variable( api_info_template, property_name, @@ -889,7 +908,9 @@ def yapi2faster(self, source_api_info): for param in req_query: param_name = param["name"] param_desc = param.get("desc", "") - api_info_template["request"]["params"]["params"][param_name] = f"${param_name}" + api_info_template["request"]["params"]["params"][ + param_name + ] = f"${param_name}" api_info_template["request"]["params"]["desc"][param_name] = param_desc api_info_template["variables"]["variables"].append({param_name: ""}) api_info_template["variables"]["desc"][param_name] = param_desc @@ -906,24 +927,32 @@ def yapi2faster(self, source_api_info): param_name = param["name"] param_desc = param.get("desc", "") param_example = param.get("example", "") - api_info_template["variables"]["variables"].append({param_name: param_example}) + api_info_template["variables"]["variables"].append( + {param_name: param_example} + ) api_info_template["variables"]["desc"][param_name] = param_desc return api_info_template - def set_ordinary_variable(self, api_info_template, field_name, field_type, field_value): + def set_ordinary_variable( + self, api_info_template, field_name, field_type, field_value + ): api_info_template["request"]["json"][field_name] = f"${field_name}" api_info_template["variables"]["variables"].append( {field_name: self.get_variable_default_value(field_type, field_value)} ) - api_info_template["variables"]["desc"][field_name] = field_value.get("description", "") + api_info_template["variables"]["desc"][field_name] = field_value.get( + "description", "" + ) def get_parsed_apis(self, api_info): """ 批量创建faster的api """ - apis = [self.yapi2faster(api) for api in api_info if isinstance(api, dict) is True] + apis = [ + self.yapi2faster(api) for api in api_info if isinstance(api, dict) is True + ] proj = models.Project.objects.get(id=self.fast_project_id) obj = models.Relation.objects.get(project_id=self.fast_project_id, type=1) eval_tree: list = eval(obj.tree) @@ -960,7 +989,9 @@ def merge_api(self, parsed_apis, imported_apis): 2、yapi的id已经存在测试平台,新获取的parsed_api.ypai_up_time > imported_api.ypai_up_time """ imported_apis_mapping = {api.yapi_id: api.ypai_up_time for api in imported_apis} - imported_apis_index = {api.yapi_id: index for index, api in enumerate(imported_apis)} + imported_apis_index = { + api.yapi_id: index for index, api in enumerate(imported_apis) + } new_apis = [] update_apis = [] diff --git a/fastrunner/utils/prepare.py b/fastrunner/utils/prepare.py index 5cc81081..3b4964d1 100644 --- a/fastrunner/utils/prepare.py +++ b/fastrunner/utils/prepare.py @@ -3,13 +3,13 @@ import pydash import requests -from django.db.models import Count, Q, Sum +from django.db.models import Sum, Count, Q from django.db.models.functions import Concat from django_celery_beat.models import PeriodicTask as celery_models - # from loguru import logger + from fastrunner import models -from fastrunner.utils.day import get_day, get_month, get_week +from fastrunner.utils.day import get_day, get_week, get_month from fastrunner.utils.parser import Format logger = logging.getLogger(__name__) @@ -183,11 +183,7 @@ def aggregate_reports_or_case_bydate(date_type, model): def get_daily_count(project_id, model_name, start, end): # 生成日期list, ['08-12', '08-13', ...] recent_days = [get_day(n)[5:] for n in range(start, end)] - models_mapping = { - "api": models.API, - "case": models.Case, - "report": models.Report, - } + models_mapping = {"api": models.API, "case": models.Case, "report": models.Report} model = models_mapping[model_name] query = model.objects if model_name == "api": @@ -196,8 +192,7 @@ def get_daily_count(project_id, model_name, start, end): # 统计给定日期范围内,每天创建的条数 count_data: list = ( query.filter( - project_id=project_id, - create_time__range=[get_day(start), get_day(end)], + project_id=project_id, create_time__range=[get_day(start), get_day(end)] ) .extra(select={"create_time": "DATE_FORMAT(create_time,'%%m-%%d')"}) .values("create_time") @@ -205,7 +200,9 @@ def get_daily_count(project_id, model_name, start, end): .values("create_time", "counts") ) # list转dict, key是日期, value是统计数 - create_time_count_mapping = {data["create_time"]: data["counts"] for data in count_data} + create_time_count_mapping = { + data["create_time"]: data["counts"] for data in count_data + } # 日期为空的key,补0 count = [create_time_count_mapping.get(d, 0) for d in recent_days] @@ -234,10 +231,7 @@ def get_project_detail_v2(pk): "count": api_create_type_count, }, "case_count_by_tag": {"tag": case_tag, "count": case_tag_count}, - "report_count_by_type": { - "type": report_type, - "count": report_type_count, - }, + "report_count_by_type": {"type": report_type, "count": report_type_count}, "daily_create_count": daily_create_count, } return res @@ -260,7 +254,9 @@ def get_jira_core_case_cover_rate(pk) -> dict: } try: # TODO 分页查找所有的核心case - res = requests.post(url=base_url, headers=headers, data=json.dumps(data)).json() + res = requests.post( + url=base_url, headers=headers, data=json.dumps(data) + ).json() err = res.get("errorMessages") if err: logger.error(err) @@ -279,7 +275,9 @@ def get_jira_core_case_cover_rate(pk) -> dict: if jira_core_case_count == 0: core_case_cover_rate = "0.00" else: - core_case_cover_rate = "%.2f" % ((covered_case_count / jira_core_case_count) * 100) + core_case_cover_rate = "%.2f" % ( + (covered_case_count / jira_core_case_count) * 100 + ) return { "jira_core_case_count": jira_core_case_count, @@ -307,7 +305,9 @@ def get_project_detail(pk): task_query_set = task_query_set.filter(enabled=1).values("args") for i in task_query_set: case_id += eval(i.get("args")) - case_step_count = models.Case.objects.filter(pk__in=case_id).aggregate(Sum("length")) + case_step_count = models.Case.objects.filter(pk__in=case_id).aggregate( + Sum("length") + ) return { "api_count": api_count, @@ -373,6 +373,7 @@ def update_casestep(body, case, username): step_list = list(models.CaseStep.objects.filter(case=case).values("id")) for index in range(len(body)): + test = body[index] try: format_http = Format(test["newBody"]) @@ -419,7 +420,9 @@ def update_casestep(body, case, username): } # is_copy is True表示用例步骤是复制的 if "case" in test.keys() and test.pop("is_copy", False) is False: - models.CaseStep.objects.filter(id=test["id"]).update(**kwargs, updater=username) + models.CaseStep.objects.filter(id=test["id"]).update( + **kwargs, updater=username + ) step_list.remove({"id": test["id"]}) else: kwargs["case"] = case @@ -445,6 +448,7 @@ def generate_casestep(body, case, username): # index也是case step的执行顺序 case_steps: list = [] for index in range(len(body)): + test = body[index] try: format_http = Format(test["newBody"]) diff --git a/fastrunner/utils/relation.py b/fastrunner/utils/relation.py index 62eb9b9e..4890dff3 100644 --- a/fastrunner/utils/relation.py +++ b/fastrunner/utils/relation.py @@ -1,33 +1,33 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: relation.py +# @File: relation.py # @Time : 2019/5/27 10:16 # @Email: lihuacai168@gmail.com # @Software: PyCharm # api模块和数据库api表relation对应关系 -API_RELATION = { - "default": 66, - "energy.ball": 67, - "manage": 68, - "app_manage": 68, - "artisan": 69, - "goods": 70, - "member": 71, - "order": 72, - "seller": 73, - "payment": 74, - "martketing": 75, - "promotion": 76, - "purchase": 77, - "security": 78, - "logistics": 79, - "recycle": 80, - "image-search": 81, - "content": 82, - "bmpm": 83, - "bi": 84, -} +API_RELATION = {"default": 66, + "energy.ball": 67, + "manage": 68, + "app_manage": 68, + "artisan": 69, + "goods": 70, + "member": 71, + "order": 72, + "seller": 73, + "payment": 74, + "martketing": 75, + "promotion": 76, + "purchase": 77, + "security": 78, + "logistics": 79, + "recycle": 80, + "image-search": 81, + "content": 82, + "bmpm": 83, + "bi": 84 + } # Java同学项目分组 API_AUTHOR = { @@ -37,15 +37,15 @@ "zhanghengjian": 87, "fengzhenwen": 88, "lingyunlong": 89, - "chencanzhang": 90, + "chencanzhang": 90 } -NIL = "无参数" -SIGN = "time,rode,sign" -SIGN_OR_TOKEN = SIGN + "(wb-token可选)" -SIGN_AND_TOKEN = SIGN + ",wb-token" -SESSION = "cookie: wb_sess:xxxxxx" -COOKIE = "cookie: wbiao.securityservice.tokenid:xxxx" +NIL = '无参数' +SIGN = 'time,rode,sign' +SIGN_OR_TOKEN = SIGN + '(wb-token可选)' +SIGN_AND_TOKEN = SIGN + ',wb-token' +SESSION = 'cookie: wb_sess:xxxxxx' +COOKIE = 'cookie: wbiao.securityservice.tokenid:xxxx' API_AUTH = { "0": ["NIL", NIL], diff --git a/fastrunner/utils/response.py b/fastrunner/utils/response.py index 58c1189a..98eecb51 100644 --- a/fastrunner/utils/response.py +++ b/fastrunner/utils/response.py @@ -23,54 +23,26 @@ class StandResponse(ErrorMsg, GenericModel, Generic[GenericResultsType]): PROJECT_NOT_EXISTS = {"code": "0102", "success": False, "msg": "项目不存在"} -DEBUGTALK_NOT_EXISTS = { - "code": "0102", - "success": False, - "msg": "miss debugtalk", -} +DEBUGTALK_NOT_EXISTS = {"code": "0102", "success": False, "msg": "miss debugtalk"} -DEBUGTALK_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "debugtalk更新成功", -} +DEBUGTALK_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "debugtalk更新成功"} -PROJECT_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "项目更新成功", -} +PROJECT_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "项目更新成功"} -PROJECT_DELETE_SUCCESS = { - "code": "0003", - "success": True, - "msg": "项目删除成功", -} +PROJECT_DELETE_SUCCESS = {"code": "0003", "success": True, "msg": "项目删除成功"} SYSTEM_ERROR = {"code": "9999", "success": False, "msg": "System Error"} TREE_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "树形结构添加成功"} -TREE_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "树形结构更新成功", -} +TREE_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "树形结构更新成功"} KEY_MISS = {"code": "0100", "success": False, "msg": "请求数据非法"} FILE_UPLOAD_SUCCESS = {"code": "0001", "success": True, "msg": "文件上传成功"} -FILE_EXISTS = { - "code": "0101", - "success": False, - "msg": "文件已存在,默认使用已有文件", -} -YAPI_ADD_SUCCESS = { - "code": "0001", - "success": True, - "msg": "导入YAPI接口添加成功", -} +FILE_EXISTS = {"code": "0101", "success": False, "msg": "文件已存在,默认使用已有文件"} +YAPI_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "导入YAPI接口添加成功"} YAPI_ADD_FAILED = {"code": "0103", "success": False, "msg": "导入YAPI接口失败"} @@ -100,104 +72,44 @@ class StandResponse(ErrorMsg, GenericModel, Generic[GenericResultsType]): CASE_SPILT_SUCCESS = {"code": "0001", "success": True, "msg": "用例切割成功"} -CASE_EXISTS = { - "code": "0101", - "success": False, - "msg": "此节点下已存在该用例集,请重新命名", -} +CASE_EXISTS = {"code": "0101", "success": False, "msg": "此节点下已存在该用例集,请重新命名"} CASE_NOT_EXISTS = {"code": "0102", "success": False, "msg": "此用例集不存在"} -CASE_DELETE_SUCCESS = { - "code": "0003", - "success": True, - "msg": "用例集删除成功", -} +CASE_DELETE_SUCCESS = {"code": "0003", "success": True, "msg": "用例集删除成功"} CASE_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "用例更新成功"} -CASE_STEP_SYNC_SUCCESS = { - "code": "0002", - "success": True, - "msg": "用例步骤同步成功", -} -CONFIG_EXISTS = { - "code": "0101", - "success": False, - "msg": "此配置已存在,请重新命名", -} +CASE_STEP_SYNC_SUCCESS = {"code": "0002", "success": True, "msg": "用例步骤同步成功"} +CONFIG_EXISTS = {"code": "0101", "success": False, "msg": "此配置已存在,请重新命名"} -VARIABLES_EXISTS = { - "code": "0101", - "success": False, - "msg": "此变量已存在,请重新命名", -} +VARIABLES_EXISTS = {"code": "0101", "success": False, "msg": "此变量已存在,请重新命名"} CONFIG_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "环境添加成功"} -VARIABLES_ADD_SUCCESS = { - "code": "0001", - "success": True, - "msg": "变量添加成功", -} +VARIABLES_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "变量添加成功"} -CONFIG_NOT_EXISTS = { - "code": "0102", - "success": False, - "msg": "指定的环境不存在", -} +CONFIG_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的环境不存在"} CONFIG_MISSING = {"code": "0103", "success": False, "msg": "缺少配置文件"} -CONFIG_IS_USED = { - "code": "0104", - "success": False, - "msg": "配置文件被用例使用中,无法删除", -} +CONFIG_IS_USED = {"code": "0104", "success": False, "msg": "配置文件被用例使用中,无法删除"} -REPORT_NOT_EXISTS = { - "code": "0102", - "success": False, - "msg": "指定的报告不存在", -} +REPORT_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的报告不存在"} -VARIABLES_NOT_EXISTS = { - "code": "0102", - "success": False, - "msg": "指定的全局变量不存在", -} +VARIABLES_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的全局变量不存在"} -CONFIG_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "环境更新成功", -} +CONFIG_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "环境更新成功"} -VARIABLES_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "全局变量更新成功", -} +VARIABLES_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "全局变量更新成功"} TASK_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "定时任务新增成功"} -TASK_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "定时任务更新成功", -} +TASK_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "定时任务更新成功"} -TASK_COPY_SUCCESS = { - "code": "0003", - "success": True, - "msg": "定时任务复制成功", -} +TASK_COPY_SUCCESS = {"code": "0003", "success": True, "msg": "定时任务复制成功"} -TASK_COPY_FAILURE = { - "code": "0103", - "success": False, - "msg": "复制失败,任务名重复了", -} +TASK_COPY_FAILURE = {"code": "0103", "success": False, "msg": "复制失败,任务名重复了"} TASK_TIME_ILLEGAL = {"code": "0101", "success": False, "msg": "时间表达式非法"} @@ -209,62 +121,30 @@ class StandResponse(ErrorMsg, GenericModel, Generic[GenericResultsType]): "msg": "Gitlab项目id已存在其他项目", } -TASK_EMAIL_ILLEGAL = { - "code": "0102", - "success": False, - "msg": "请指定邮件接收人列表", -} +TASK_EMAIL_ILLEGAL = {"code": "0102", "success": False, "msg": "请指定邮件接收人列表"} TASK_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "任务删除成功"} -TASK_RUN_SUCCESS = { - "code": "0001", - "success": True, - "msg": "用例运行中,请稍后查看报告", -} +TASK_RUN_SUCCESS = {"code": "0001", "success": True, "msg": "用例运行中,请稍后查看报告"} PLAN_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "集成计划删除成功"} PLAN_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "计划添加成功"} -PLAN_KEY_EXIST = { - "code": "0101", - "success": False, - "msg": "该KEY值已存在,请修改KEY值", -} +PLAN_KEY_EXIST = {"code": "0101", "success": False, "msg": "该KEY值已存在,请修改KEY值"} -PLAN_ILLEGAL = { - "code": "0101", - "success": False, - "msg": "提取字段格式错误,请检查", -} +PLAN_ILLEGAL = {"code": "0101", "success": False, "msg": "提取字段格式错误,请检查"} PLAN_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "计划更新成功"} -HOSTIP_EXISTS = { - "code": "0101", - "success": False, - "msg": "此域名已存在,请重新命名", -} +HOSTIP_EXISTS = {"code": "0101", "success": False, "msg": "此域名已存在,请重新命名"} HOSTIP_ADD_SUCCESS = {"code": "0001", "success": True, "msg": "域名添加成功"} -HOSTIP_NOT_EXISTS = { - "code": "0102", - "success": False, - "msg": "指定的域名不存在", -} +HOSTIP_NOT_EXISTS = {"code": "0102", "success": False, "msg": "指定的域名不存在"} -HOSTIP_EXISTS = { - "code": "0101", - "success": False, - "msg": "此域名已存在,请重新命名", -} +HOSTIP_EXISTS = {"code": "0101", "success": False, "msg": "此域名已存在,请重新命名"} -HOSTIP_UPDATE_SUCCESS = { - "code": "0002", - "success": True, - "msg": "域名更新成功", -} +HOSTIP_UPDATE_SUCCESS = {"code": "0002", "success": True, "msg": "域名更新成功"} HOST_DEL_SUCCESS = {"code": "0003", "success": True, "msg": "域名删除成功"} diff --git a/fastrunner/utils/runner.py b/fastrunner/utils/runner.py index 40278394..ee222a90 100644 --- a/fastrunner/utils/runner.py +++ b/fastrunner/utils/runner.py @@ -1,29 +1,30 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- -import os import shutil -import subprocess import sys +import os +import subprocess import tempfile - -from FasterRunner.settings.base import BASE_DIR from fastrunner.utils import loader - +from FasterRunner.settings.base import BASE_DIR EXEC = sys.executable -if "uwsgi" in EXEC: +if 'uwsgi' in EXEC: # 修复虚拟环境下,用uwsgi执行时,PYTHONPATH还是用了系统默认的 - EXEC = EXEC.replace("uwsgi", "python") + EXEC = EXEC.replace("uwsgi", 'python') + +class DebugCode(object): -class DebugCode: def __init__(self, code): self.__code = code self.resp = None - self.temp = tempfile.mkdtemp(prefix="FasterRunner") + self.temp = tempfile.mkdtemp(prefix='FasterRunner') def run(self): - """dumps debugtalk.py and run""" + """ dumps debugtalk.py and run + """ try: os.chdir(self.temp) file_path = os.path.join(self.temp, "debugtalk.py") @@ -31,28 +32,21 @@ def run(self): # 修复驱动代码运行时,找不到内置httprunner包 run_path = [BASE_DIR] run_path.extend(sys.path) - env = {"PYTHONPATH": ":".join(run_path)} - self.resp = decode( - subprocess.check_output( - [EXEC, file_path], - stderr=subprocess.STDOUT, - timeout=60, - env=env, - ) - ) + env = {'PYTHONPATH': ':'.join(run_path)} + self.resp = decode(subprocess.check_output([EXEC, file_path], stderr=subprocess.STDOUT, timeout=60, env=env)) except subprocess.CalledProcessError as e: self.resp = decode(e.output) except subprocess.TimeoutExpired: - self.resp = "RunnerTimeOut" + self.resp = 'RunnerTimeOut' os.chdir(BASE_DIR) shutil.rmtree(self.temp) def decode(s): try: - return s.decode("utf-8") + return s.decode('utf-8') except UnicodeDecodeError: - return s.decode("gbk") + return s.decode('gbk') diff --git a/fastrunner/utils/task.py b/fastrunner/utils/task.py index 99db505c..a4d689c0 100644 --- a/fastrunner/utils/task.py +++ b/fastrunner/utils/task.py @@ -1,22 +1,22 @@ import json import logging - # from loguru import logger from django_celery_beat import models as celery_models - from fastrunner.utils import response from fastrunner.utils.parser import format_json logger = logging.getLogger(__name__) -class Task: +class Task(object): """ 定时任务操作 """ def __init__(self, **kwargs): - logger.info(f"before process task data:\n {format_json(kwargs)}") + logger.info( + "before process task data:\n {kwargs}".format(kwargs=format_json(kwargs)) + ) self.__name = kwargs["name"] self.__data = kwargs["data"] self.__crontab = kwargs["crontab"] @@ -64,8 +64,11 @@ def add_task(self): """ add tasks """ - if celery_models.PeriodicTask.objects.filter(name__exact=self.__name).count() > 0: - logger.info(f"{self.__name} tasks exist") + if ( + celery_models.PeriodicTask.objects.filter(name__exact=self.__name).count() + > 0 + ): + logger.info("{name} tasks exist".format(name=self.__name)) return response.TASK_HAS_EXISTS if self.__email["strategy"] == "始终发送" or self.__email["strategy"] == "仅失败发送": @@ -75,9 +78,13 @@ def add_task(self): resp = self.format_crontab() if resp["success"]: - crontab = celery_models.CrontabSchedule.objects.filter(**self.__crontab_time).first() + crontab = celery_models.CrontabSchedule.objects.filter( + **self.__crontab_time + ).first() if crontab is None: - crontab = celery_models.CrontabSchedule.objects.create(**self.__crontab_time) + crontab = celery_models.CrontabSchedule.objects.create( + **self.__crontab_time + ) task, created = celery_models.PeriodicTask.objects.get_or_create( name=self.__name, task=self.__task, crontab=crontab ) @@ -87,7 +94,7 @@ def add_task(self): task.kwargs = json.dumps(self.__email, ensure_ascii=False) task.description = self.__project task.save() - logger.info(f"{self.__name} tasks save success") + logger.info("{name} tasks save success".format(name=self.__name)) return response.TASK_ADD_SUCCESS else: return resp @@ -96,9 +103,13 @@ def update_task(self, pk): resp = self.format_crontab() if resp["success"]: task = celery_models.PeriodicTask.objects.get(id=pk) - crontab = celery_models.CrontabSchedule.objects.filter(**self.__crontab_time).first() + crontab = celery_models.CrontabSchedule.objects.filter( + **self.__crontab_time + ).first() if crontab is None: - crontab = celery_models.CrontabSchedule.objects.create(**self.__crontab_time) + crontab = celery_models.CrontabSchedule.objects.create( + **self.__crontab_time + ) task.crontab = crontab task.enabled = self.__switch task.args = json.dumps(self.__data, ensure_ascii=False) @@ -106,7 +117,7 @@ def update_task(self, pk): task.description = self.__project task.name = self.__name task.save() - logger.info(f"{self.__name} tasks save success") + logger.info("{name} tasks save success".format(name=self.__name)) return response.TASK_UPDATE_SUCCESS else: return resp diff --git a/fastrunner/utils/test_tree.py b/fastrunner/utils/test_tree.py index d7169ed3..cc69da0a 100644 --- a/fastrunner/utils/test_tree.py +++ b/fastrunner/utils/test_tree.py @@ -1,47 +1,27 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: test_tree.py +# @File: test_tree.py # @Time : 2020/1/3 1:13 # @Email: lihuacai168@gmail.com # @Software: PyCharm from unittest import TestCase - -from .tree import ( - get_all_ycatid, - get_faster_id_by_ycatid, - get_tree_label, - get_tree_max_id, - get_tree_max_id_old, - get_tree_relation_name, - get_tree_ycatid_mapping, -) +from .tree import get_tree_label , get_all_ycatid , get_faster_id_by_ycatid , get_tree_ycatid_mapping, get_tree_relation_name, get_tree_max_id, get_tree_max_id_old class Test(TestCase): - tree = [ - {"id": 1, "label": "默认分组", "children": []}, - { - "id": 2, - "label": "不带token", - "children": [ - { - "id": 3, - "label": "杂志", - "children": [dict(id=323, label="个人中心", children=[])], - }, - {"id": 4, "label": "我的页面", "children": []}, - {"id": 5, "label": "购物车", "children": []}, - {"id": 6, "label": "选表", "children": []}, - {"id": 7, "label": "店铺", "children": []}, - {"id": 11, "label": "首页", "children": []}, - {"id": 13, "label": "商品详情", "children": []}, - {"id": 15, "label": "启动页", "children": []}, - ], - }, - {"id": 30, "label": "M站", "children": []}, - {"id": 34, "label": "注册", "children": []}, - ] + tree = [{'id': 1, 'label': '默认分组', 'children': []}, + {'id': 2, 'label': '不带token', 'children': [{'id': 3, 'label': '杂志', 'children': [ + dict(id=323, label='个人中心', children=[])]}, + {'id': 4, 'label': '我的页面', 'children': []}, + {'id': 5, 'label': '购物车', 'children': []}, + {'id': 6, 'label': '选表', 'children': []}, + {'id': 7, 'label': '店铺', 'children': []}, + {'id': 11, 'label': '首页', 'children': []}, + {'id': 13, 'label': '商品详情', 'children': []}, + {'id': 15, 'label': '启动页', 'children': []}]}, + {'id': 30, 'label': 'M站', 'children': []}, {'id': 34, 'label': '注册', 'children': []}] def test_get_tree_max_id(self): assert get_tree_max_id(self.tree) == 323 @@ -53,42 +33,28 @@ def test_get_tree_max_id_empty(self): assert get_tree_max_id([]) == 0 def test_get_tree_label_default(self): - assert get_tree_label(self.tree, "默认分组") == 1 + assert get_tree_label(self.tree, '默认分组') == 1 def test_get_tree_label(self): - assert get_tree_label(self, search_label="tree为空") == 1 + assert get_tree_label(self, search_label='tree为空') == 1 def test_get_tree_label_children(self): - assert get_tree_label(self.tree, "杂志") == 3 + assert get_tree_label(self.tree, '杂志') == 3 def test_get_tree_label_second_children(self): - assert get_tree_label(self.tree, "个人中心") == 323 + assert get_tree_label(self.tree, '个人中心') == 323 def test_get_tree_relation_name_default(self): - assert get_tree_relation_name(self.tree, 1) == "默认分组" + assert get_tree_relation_name(self.tree, 1) == '默认分组' def test_get_tree_relation_name_default(self): - assert get_tree_relation_name(self.tree, 11) == "首页" + assert get_tree_relation_name(self.tree, 11) == '首页' class TestYAPITree(TestCase): - tree = [ - { - "id": 1, - "yapi_catid": 100, - "label": "测试分组1", - "children": [ - { - "id": 2, - "yapi_catid": 101, - "label": "聚合数据", - "children": [], - }, - {"id": 3, "label": "eee", "children": []}, - ], - }, - {"id": 30, "yapi_catid": 102, "label": "222", "children": []}, - ] + tree = [{'id': 1, 'yapi_catid': 100, 'label': '测试分组1', + 'children': [{'id': 2, 'yapi_catid': 101, 'label': '聚合数据', 'children': []}, + {'id': 3, 'label': 'eee', 'children': []}]}, {'id': 30, 'yapi_catid': 102, 'label': '222', 'children': []}] def test_get_all_ycatid_default(self): assert get_all_ycatid(self.tree) == [100, 101, 102] diff --git a/fastrunner/utils/tree.py b/fastrunner/utils/tree.py index b87cab9e..f2b23f0f 100644 --- a/fastrunner/utils/tree.py +++ b/fastrunner/utils/tree.py @@ -10,11 +10,11 @@ def get_tree_max_id_old(value, list_id=[]): if isinstance(value, list): for content in value: # content -> dict - children = content.get("children") + children = content.get('children') if children: get_tree_max_id_old(children) - list_id.append(content["id"]) + list_id.append(content['id']) return max(list_id) @@ -29,8 +29,8 @@ def get_tree_max_id(tree: list): while len(queue) != 0: sub_tree: list = queue.popleft() for node in sub_tree: - children: list = node.get("children") - max_id = max(max_id, node["id"]) + children: list = node.get('children') + max_id = max(max_id, node['id']) # 有子节点 if len(children) > 0: queue.append(children) @@ -46,11 +46,11 @@ def get_all_ycatid(value, list_id=[]): if isinstance(value, list): for content in value: # content -> dict - yapi_catid = content.get("yapi_catid") + yapi_catid = content.get('yapi_catid') if yapi_catid: list_id.append(yapi_catid) - children = content.get("children") + children = content.get('children') if children: get_all_ycatid(children) return list_id @@ -66,9 +66,9 @@ def get_faster_id_by_ycatid(value, yapi_catid): if isinstance(value, list): for content in value: # content -> dict - if content.get("yapi_catid") == yapi_catid: - return content["id"] - children = content.get("children") + if content.get('yapi_catid') == yapi_catid: + return content['id'] + children = content.get('children') if children: get_faster_id_by_ycatid(children, yapi_catid) return 0 @@ -84,24 +84,25 @@ def get_tree_ycatid_mapping(value, mapping={}): if isinstance(value, list): for content in value: # content -> dict - yapi_catid = content.get("yapi_catid") + yapi_catid = content.get('yapi_catid') if yapi_catid: - mapping.update({yapi_catid: content.get("id")}) - children = content.get("children") + mapping.update({yapi_catid: content.get('id')}) + children = content.get('children') if children: get_tree_ycatid_mapping(children, mapping) return mapping def get_file_size(size): - """计算大小""" + """计算大小 + """ if size >= 1048576: - size = str(round(size / 1048576, 2)) + "MB" + size = str(round(size / 1048576, 2)) + 'MB' elif size >= 1024: - size = str(round(size / 1024, 2)) + "KB" + size = str(round(size / 1024, 2)) + 'KB' else: - size = str(size) + "Byte" + size = str(size) + 'Byte' return size @@ -123,15 +124,15 @@ def get_tree_label(value, search_label): if isinstance(value, list): for content in value: # content -> dict - if content["label"] == search_label: - label_id = content["id"] - children = content.get("children") + if content['label'] == search_label: + label_id = content['id'] + children = content.get('children') if children: get_tree_label(children, search_label) return label_id -label = "" +label = '' def get_tree_relation_name(value, relation_id): @@ -144,9 +145,9 @@ def get_tree_relation_name(value, relation_id): if isinstance(value, list): for content in value: # content -> dict - if content["id"] == relation_id: - label = content["label"] - children = content.get("children") + if content['id'] == relation_id: + label = content['label'] + children = content.get('children') if children: get_tree_relation_name(children, relation_id) return label diff --git a/fastrunner/views/api.py b/fastrunner/views/api.py index ce61dd66..a60fb167 100644 --- a/fastrunner/views/api.py +++ b/fastrunner/views/api.py @@ -1,32 +1,32 @@ import datetime -import coreapi from django.core.exceptions import ObjectDoesNotExist -from django.db import DataError -from django.db.models import Q from django.utils.decorators import method_decorator from drf_yasg.utils import swagger_auto_schema from rest_framework import status -from rest_framework.response import Response -from rest_framework.schemas import AutoSchema from rest_framework.viewsets import GenericViewSet - from fastrunner import models, serializers +from rest_framework.response import Response from fastrunner.utils import response from fastrunner.utils.decorator import request_log from fastrunner.utils.parser import Format, Parse +from django.db import DataError +from django.db.models import Q + +from rest_framework.schemas import AutoSchema, SchemaGenerator +import coreapi class APITemplateViewSchema(AutoSchema): def get_manual_fields(self, path, method): extra_fields = [] - if method.lower() in ("get",): + if method.lower() in ('get',): extra_fields = [ - coreapi.Field("node"), - coreapi.Field("project"), - coreapi.Field("search"), - coreapi.Field("tag"), - coreapi.Field("rigEnv"), + coreapi.Field('node'), + coreapi.Field('project'), + coreapi.Field('search'), + coreapi.Field('tag'), + coreapi.Field('rigEnv'), ] manual_fields = super().get_manual_fields(path, method) return manual_fields + extra_fields @@ -36,13 +36,12 @@ class APITemplateView(GenericViewSet): """ API操作视图 """ - serializer_class = serializers.APISerializer queryset = models.API.objects.filter(~Q(tag=4)) schema = APITemplateViewSchema() @swagger_auto_schema(query_serializer=serializers.AssertSerializer) - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def list(self, request): """ api-获取api列表 @@ -51,17 +50,17 @@ def list(self, request): """ ser = serializers.AssertSerializer(data=request.query_params) if ser.is_valid(): - node = ser.validated_data.get("node") - project = ser.validated_data.get("project") - search: str = ser.validated_data.get("search") - tag = ser.validated_data.get("tag") - rig_env = ser.validated_data.get("rigEnv") - delete = ser.validated_data.get("delete") - only_me = ser.validated_data.get("onlyMe") - showYAPI = ser.validated_data.get("showYAPI") - creator = ser.validated_data.get("creator") - - queryset = self.get_queryset().filter(project__id=project, delete=delete).order_by("-update_time") + node = ser.validated_data.get('node') + project = ser.validated_data.get('project') + search: str = ser.validated_data.get('search') + tag = ser.validated_data.get('tag') + rig_env = ser.validated_data.get('rigEnv') + delete = ser.validated_data.get('delete') + only_me = ser.validated_data.get('onlyMe') + showYAPI = ser.validated_data.get('showYAPI') + creator = ser.validated_data.get('creator') + + queryset = self.get_queryset().filter(project__id=project, delete=delete).order_by('-update_time') if only_me is True: queryset = queryset.filter(creator=request.user) @@ -70,20 +69,20 @@ def list(self, request): queryset = queryset.filter(creator=creator) if showYAPI is False: - queryset = queryset.filter(~Q(creator="yapi")) + queryset = queryset.filter(~Q(creator='yapi')) - if search != "": + if search != '': search: list = search.split() for key in search: queryset = queryset.filter(Q(name__contains=key) | Q(url__contains=key)) - if node != "": + if node != '': queryset = queryset.filter(relation=node) - if tag != "": + if tag != '': queryset = queryset.filter(tag=tag) - if rig_env != "": + if rig_env != '': queryset = queryset.filter(rig_env=rig_env) pagination_queryset = self.paginate_queryset(queryset) @@ -93,7 +92,7 @@ def list(self, request): else: return Response(ser.errors, status=status.HTTP_400_BAD_REQUEST) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add(self, request): """ api-新增一个api @@ -105,13 +104,13 @@ def add(self, request): api.parse() api_body = { - "name": api.name, - "body": api.testcase, - "url": api.url, - "method": api.method, - "project": models.Project.objects.get(id=api.project), - "relation": api.relation, - "creator": request.user.username, + 'name': api.name, + 'body': api.testcase, + 'url': api.url, + 'method': api.method, + 'project': models.Project.objects.get(id=api.project), + 'relation': api.relation, + 'creator': request.user.username } try: @@ -121,23 +120,23 @@ def add(self, request): return Response(response.API_ADD_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def update(self, request, **kwargs): """ api-更新单个api 更新单个api的内容 """ - pk = kwargs["pk"] + pk = kwargs['pk'] api = Format(request.data) api.parse() api_body = { - "name": api.name, - "body": api.testcase, - "url": api.url, - "method": api.method, - "updater": request.user.username, + 'name': api.name, + 'body': api.testcase, + 'url': api.url, + 'method': api.method, + 'updater': request.user.username } try: @@ -147,34 +146,37 @@ def update(self, request, **kwargs): return Response(response.API_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def move(self, request): """ api-批量更新api的目录 移动api到指定目录 """ - project: int = request.data.get("project") - relation: int = request.data.get("relation") - apis: list = request.data.get("api") - ids = [api["id"] for api in apis] + project: int = request.data.get('project') + relation: int = request.data.get('relation') + apis: list = request.data.get('api') + ids = [api['id'] for api in apis] try: - models.API.objects.filter(project=project, id__in=ids).update(relation=relation) + models.API.objects.filter( + project=project, + id__in=ids + ).update(relation=relation) except ObjectDoesNotExist: return Response(response.API_NOT_FOUND) return Response(response.API_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def copy(self, request, **kwargs): """ api-复制api 复制一个api """ - pk = kwargs["pk"] - name = request.data["name"] + pk = kwargs['pk'] + name = request.data['name'] api = models.API.objects.get(id=pk) body = eval(api.body) body["name"] = name @@ -186,7 +188,7 @@ def copy(self, request, **kwargs): api.save() return Response(response.API_ADD_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def delete(self, request, **kwargs): """ api-删除一个api @@ -195,40 +197,40 @@ def delete(self, request, **kwargs): """ try: - if kwargs.get("pk"): # 单个删除 + if kwargs.get('pk'): # 单个删除 # models.API.objects.get(id=kwargs['pk']).delete() - models.API.objects.filter(id=kwargs["pk"]).update(delete=1, update_time=datetime.datetime.now()) + models.API.objects.filter(id=kwargs['pk']).update(delete=1, update_time=datetime.datetime.now()) else: for content in request.data: # models.API.objects.get(id=content['id']).delete() - models.API.objects.filter(id=content["id"]).update(delete=1) + models.API.objects.filter(id=content['id']).update(delete=1) except ObjectDoesNotExist: return Response(response.API_NOT_FOUND) return Response(response.API_DEL_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add_tag(self, request, **kwargs): """ api-更新api的tag,暂时默认为调试成功 更新api的tag类型 """ - api_ids: list = request.data.get("api_ids", []) + api_ids: list = request.data.get('api_ids', []) try: if api_ids: models.API.objects.filter(pk__in=api_ids).update( - tag=request.data["tag"], + tag=request.data['tag'], update_time=datetime.datetime.now(), - updater=request.user.username, + updater=request.user.username ) except ObjectDoesNotExist: return Response(response.API_NOT_FOUND) return Response(response.API_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def sync_case(self, request, **kwargs): """ api-同步api的到case_step @@ -237,28 +239,32 @@ def sync_case(self, request, **kwargs): 2.根据api_id更新case_step中的("name", "body", "url", "method", "updater") 3.更新case的update_time, updater """ - pk = kwargs["pk"] + pk = kwargs['pk'] source_api = models.API.objects.filter(pk=pk).values("name", "body", "url", "method", "project").first() # 根据api反向查出project project = source_api.pop("project") - project_case_ids = models.Case.objects.filter(project=project).values_list("id", flat=True) + project_case_ids = models.Case.objects.filter(project=project).values_list('id', flat=True) # 限制case_step只在当前项目 case_steps = models.CaseStep.objects.filter(source_api_id=pk, case_id__in=project_case_ids) case_steps.update( **source_api, updater=request.user.username, - update_time=datetime.datetime.now(), + update_time=datetime.datetime.now() ) - case_ids = case_steps.values_list("case", flat=True) + case_ids = case_steps.values_list('case', flat=True) # 限制case只在当前项目 - models.Case.objects.filter(pk__in=list(case_ids), project=project).update( - update_time=datetime.datetime.now(), updater=request.user.username + models.Case.objects.filter( + pk__in=list(case_ids), + project=project + ).update( + update_time=datetime.datetime.now(), + updater=request.user.username ) return Response(response.CASE_STEP_SYNC_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def single(self, request, **kwargs): """ api-获取单个api详情,返回body信息 @@ -266,7 +272,7 @@ def single(self, request, **kwargs): 获取单个api的详细情况 """ try: - api = models.API.objects.get(id=kwargs["pk"]) + api = models.API.objects.get(id=kwargs['pk']) except ObjectDoesNotExist: return Response(response.API_NOT_FOUND) @@ -274,12 +280,12 @@ def single(self, request, **kwargs): parse.parse_http() resp = { - "id": api.id, - "body": parse.testcase, - "success": True, - "creator": api.creator, - "relation": api.relation, - "project": api.project.id, + 'id': api.id, + 'body': parse.testcase, + 'success': True, + 'creator': api.creator, + 'relation': api.relation, + 'project': api.project.id, } return Response(resp) diff --git a/fastrunner/views/api_rig.py b/fastrunner/views/api_rig.py index 5718db9a..01217baa 100644 --- a/fastrunner/views/api_rig.py +++ b/fastrunner/views/api_rig.py @@ -1,28 +1,28 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: api_rig.py +# @File: api_rig.py # @Time : 2019/5/25 9:25 # @Email: lihuacai168@gmail.com # @Software: PyCharm import datetime -from django.core.exceptions import ObjectDoesNotExist -from django.db import DataError from django.db.models import Q +from django.core.exceptions import ObjectDoesNotExist from django.utils.decorators import method_decorator -from rest_framework import exceptions -from rest_framework.authentication import BaseAuthentication -from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet - from fastrunner import models, serializers +from rest_framework.response import Response from fastrunner.utils import response from fastrunner.utils.decorator import request_log from fastrunner.utils.parser import Format -from fastrunner.utils.relation import API_AUTHOR, API_RELATION -from fastrunner.views import run +from django.db import DataError +from rest_framework import exceptions +from rest_framework.authentication import BaseAuthentication from fastuser import models as user_model +from fastrunner.utils.relation import API_RELATION, API_AUTHOR +from fastrunner.views import run class Authenticator(BaseAuthentication): @@ -35,7 +35,11 @@ def authenticate(self, request): obj = user_model.UserToken.objects.filter(token=token).first() if not obj: - raise exceptions.AuthenticationFailed({"code": "9999", "msg": "用户未认证", "success": False}) + raise exceptions.AuthenticationFailed({ + "code": "9999", + "msg": "用户未认证", + "success": False + }) # valid update valid time obj.token = token obj.save() @@ -43,7 +47,7 @@ def authenticate(self, request): return obj.user, obj def authenticate_header(self, request): - return "Auth Failed" + return 'Auth Failed' class APIRigView(GenericViewSet): @@ -63,13 +67,13 @@ def list(self, request): project = request.query_params["project"] search = request.query_params["search"] # queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') - queryset = self.get_queryset().filter(project__id=project, delete=0).order_by("-update_time") + queryset = self.get_queryset().filter(project__id=project, delete=0).order_by('-update_time') # queryset = self.get_queryset().filter(Q(project__id=project) and ~Q(delete=1)).order_by('-update_time') - if search != "": + if search != '': queryset = queryset.filter(name__contains=search) - if node != "": + if node != '': queryset = queryset.filter(relation=node) pagination_queryset = self.paginate_queryset(queryset) @@ -77,85 +81,85 @@ def list(self, request): return self.get_paginated_response(serializer.data) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add(self, request): """ - 新增一个接口 - { - "header": { - "header": { - "wb-token": "$wb_token" - }, - "desc": { - "wb-token": "用户登陆token" - } - }, - "request": { - "form": { - "data": {}, - "desc": {} - }, - "json": {}, - "params": { - "params": { - "goodsCode": "42470" - }, - "desc": { - "goodsCode": "商品编码" - } - }, - "files": { - "files": {}, - "desc": {} - } - }, - "extract": { - "extract": [], - "desc": {} - }, - "validate": { - "validate": [{"equals": ["content.info.error",0]}] - }, - "variables": { - "variables": [ - { - "auth_type": "APP_MEMBER_AUTH" - }, - { - "rpc_Group": "wbiao.seller.prod" - }, - { - "rpc_Interface": "cn.wbiao.seller.api.GoodsDetailService" - }, - { - "params_type": "Key_Value" - }, - { - "author": "xuqirong" - } - ], - "desc": { - "auth_type": "认证类型", - "rpc_Group": "RPC服务组", - "rpc_Interface": "后端服务接口", - "params_type": "入参数形式", - "author": "作者" - } - }, - "hooks": { - "setup_hooks": [ - "${get_sign($request,$auth_type)}" - ], - "teardown_hooks": [] - }, - "url": "/wxmp/mall/goods/detail/getRecommendGoodsList", - "method": "GET", - "name": "查询关联的商品推荐列表-小程序需签名", - "times": 1, - "nodeId": "member", - "project": 5, - "rig_id":200014 - } + 新增一个接口 + { + "header": { + "header": { + "wb-token": "$wb_token" + }, + "desc": { + "wb-token": "用户登陆token" + } + }, + "request": { + "form": { + "data": {}, + "desc": {} + }, + "json": {}, + "params": { + "params": { + "goodsCode": "42470" + }, + "desc": { + "goodsCode": "商品编码" + } + }, + "files": { + "files": {}, + "desc": {} + } + }, + "extract": { + "extract": [], + "desc": {} + }, + "validate": { + "validate": [{"equals": ["content.info.error",0]}] + }, + "variables": { + "variables": [ + { + "auth_type": "APP_MEMBER_AUTH" + }, + { + "rpc_Group": "wbiao.seller.prod" + }, + { + "rpc_Interface": "cn.wbiao.seller.api.GoodsDetailService" + }, + { + "params_type": "Key_Value" + }, + { + "author": "xuqirong" + } + ], + "desc": { + "auth_type": "认证类型", + "rpc_Group": "RPC服务组", + "rpc_Interface": "后端服务接口", + "params_type": "入参数形式", + "author": "作者" + } + }, + "hooks": { + "setup_hooks": [ + "${get_sign($request,$auth_type)}" + ], + "teardown_hooks": [] + }, + "url": "/wxmp/mall/goods/detail/getRecommendGoodsList", + "method": "GET", + "name": "查询关联的商品推荐列表-小程序需签名", + "times": 1, + "nodeId": "member", + "project": 5, + "rig_id":200014 +} """ api = Format(request.data) @@ -168,32 +172,32 @@ def add(self, request): try: relation = API_RELATION[api.relation] except KeyError: - relation = API_RELATION["default"] + relation = API_RELATION['default'] if api.rig_id: - api.name = api.name + "-" + str(api.rig_id) + api.name = api.name + '-' + str(api.rig_id) if api.rig_env == 0: - api.name += "-测试" + api.name += '-测试' elif api.rig_env == 1: - api.name += "-生产" + api.name += '-生产' # 生产环境比测试环境的关系节点大20 relation += 20 else: - api.name += "-预发布" + api.name += '-预发布' - api.testcase["name"] = api.name + api.testcase['name'] = api.name api_body = { - "name": api.name, - "body": api.testcase, - "url": api.url, - "method": api.method, - "project": models.Project.objects.get(id=api.project), + 'name': api.name, + 'body': api.testcase, + 'url': api.url, + 'method': api.method, + 'project': models.Project.objects.get(id=api.project), # 'relation': api.relation, - "rig_id": api.rig_id, - "rig_env": api.rig_env, - "relation": relation, + 'rig_id': api.rig_id, + 'rig_env': api.rig_env, + 'relation': relation } # try: # relation = API_RELATION[api.relation] @@ -204,7 +208,8 @@ def add(self, request): try: # 增加api之前先删除已经存在的相同id的除了手动调试成功的api models.API.objects.filter(rig_id=api.rig_id).filter(~Q(tag=1)).update( - delete=1, update_time=datetime.datetime.now() + delete=1, + update_time=datetime.datetime.now() ) # 创建成功,返回对象,方便获取id obj = models.API.objects.create(**api_body) @@ -213,12 +218,12 @@ def add(self, request): # api作者 # 2019年10月22日 修复rig增加api运行失败时,没有复制api到Java同学项目 - author = api_body["body"]["variables"][4]["author"] + author = api_body['body']['variables'][4]['author'] self.copy_to_java(api.rig_id, author) # api运行成功,就自动增加到用例集里面 run_result = run.auto_run_api_pk(config=api.rig_env, id=obj.id) - if run_result == "success": + if run_result == 'success': run.update_auto_case_step(**api_body) return Response(response.API_ADD_SUCCESS) @@ -229,31 +234,31 @@ def copy_to_java(self, rig_id, author): try: relation = API_AUTHOR[author] except KeyError: - relation = API_AUTHOR["default"] + relation = API_AUTHOR['default'] # Java项目的id=4 # obj = models.API.objects.get(rig_id=rig_id) # 修复已经存在的rig_id的api无法复制 - obj = models.API.objects.filter(rig_id=rig_id).order_by("-id")[0] + obj = models.API.objects.filter(rig_id=rig_id).order_by('-id')[0] obj.id = None obj.relation = relation obj.project_id = 4 obj.save() - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def update(self, request, **kwargs): """ 更新接口 """ - pk = kwargs["rig_id"] + pk = kwargs['rig_id'] api = Format(request.data) api.parse() api_body = { - "name": api.name, - "body": api.testcase, - "url": api.url, - "method": api.method, + 'name': api.name, + 'body': api.testcase, + 'url': api.url, + 'method': api.method, } try: diff --git a/fastrunner/views/ci.py b/fastrunner/views/ci.py index 171be2bb..016e7673 100644 --- a/fastrunner/views/ci.py +++ b/fastrunner/views/ci.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: ci.py @@ -10,19 +11,19 @@ import time import xmltodict -from django.conf import settings from django.http import HttpResponse -from django.utils.decorators import method_decorator +from django.conf import settings from django_celery_beat.models import PeriodicTask from drf_yasg.utils import swagger_auto_schema -from rest_framework import status -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet from fastrunner import models -from fastrunner.serializers import CIReportSerializer, CISerializer -from fastrunner.utils import lark_message, loader +from fastrunner.utils import loader, lark_message from fastrunner.utils.decorator import request_log +from django.utils.decorators import method_decorator +from rest_framework import status +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet +from fastrunner.serializers import CISerializer, CIReportSerializer from fastrunner.utils.host import parse_host from fastrunner.utils.loader import save_summary @@ -47,19 +48,15 @@ def summary2junit(summary: dict) -> dict: time_info = summary.get("time") res["testsuites"]["testsuite"]["time"] = time_info.get("duration") start_at: str = time_info.get("start_at") - datetime_str = datetime.datetime.fromtimestamp(int(float(start_at))).strftime("%Y-%m-%dT%H:%M:%S.%f") + datetime_str = datetime.datetime.fromtimestamp(int(float(start_at))).strftime( + "%Y-%m-%dT%H:%M:%S.%f" + ) res["testsuites"]["testsuite"]["timestamp"] = datetime_str details = summary.get("details", []) res["testsuites"]["testsuite"]["tests"] = len(details) for detail in details: - test_case = { - "classname": "", - "file": "", - "line": "", - "name": "", - "time": "", - } + test_case = {"classname": "", "file": "", "line": "", "name": "", "time": ""} name = detail.get("name") test_case["classname"] = name # 对应junit的Suite records = detail.get("records") @@ -79,7 +76,11 @@ def summary2junit(summary: dict) -> dict: step_status = record.get("status") if step_status == "failure": failure_details.append( - f"{index}-{record['name']}" + "\n" + record.get("attachment") + "\n" + "*" * 68 + f"{index}-{record['name']}" + + "\n" + + record.get("attachment") + + "\n" + + "*" * 68 ) elif step_status == "error": case_error = True @@ -90,10 +91,7 @@ def summary2junit(summary: dict) -> dict: else: res["testsuites"]["testsuite"]["failures"] += 1 - failure = { - "message": "断言或者抽取失败", - "#text": "\n".join(failure_details), - } + failure = {"message": "断言或者抽取失败", "#text": "\n".join(failure_details)} test_case["failure"] = failure res["testsuites"]["testsuite"]["testcase"].append(test_case) return res @@ -129,13 +127,19 @@ def run_ci_tests(self, request): ci_project_ids = [ci_project_ids] # 定时任务中的ci_project_id和ci_env同时匹配 - if ci_project_id in ci_project_ids and ci_env and ci_env == kwargs.get("ci_env"): + if ( + ci_project_id in ci_project_ids + and ci_env + and ci_env == kwargs.get("ci_env") + ): enabled_task_ids.append(pk) project = pk_kwargs["description"] # 没有匹配用例,直接返回 if not enabled_task_ids: - datetime_str = datetime.datetime.fromtimestamp(int(time.time())).strftime("%Y-%m-%dT%H:%M:%S.%f") + datetime_str = datetime.datetime.fromtimestamp( + int(time.time()) + ).strftime("%Y-%m-%dT%H:%M:%S.%f") not_found_case_res = { "testsuites": { "testsuite": { @@ -166,9 +170,15 @@ def run_ci_tests(self, request): task_obj: str = query.filter(id=task_id).first() # 如果task中存在重载配置,就覆盖用例中的配置 override_config = json.loads(task_obj.kwargs).get("config") - if override_config_body is None and override_config and override_config != "请选择": + if ( + override_config_body is None + and override_config + and override_config != "请选择" + ): override_config_body = eval( - models.Config.objects.get(name=override_config, project__id=project).body + models.Config.objects.get( + name=override_config, project__id=project + ).body ) if task_obj: @@ -181,9 +191,17 @@ def run_ci_tests(self, request): else: continue # 反查出一个task中包含的所有用例 - suite = list(models.Case.objects.filter(pk__in=eval(case_ids)).order_by("id").values("id", "name")) + suite = list( + models.Case.objects.filter(pk__in=eval(case_ids)) + .order_by("id") + .values("id", "name") + ) for case in suite: - case_step_list = models.CaseStep.objects.filter(case__id=case["id"]).order_by("step").values("body") + case_step_list = ( + models.CaseStep.objects.filter(case__id=case["id"]) + .order_by("step") + .values("body") + ) testcase_list = [] for case_step in case_step_list: body = eval(case_step["body"]) @@ -195,14 +213,20 @@ def run_ci_tests(self, request): if override_config_body: config = override_config_body else: - config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) + config = eval( + models.Config.objects.get( + name=body["name"], project__id=project + ).body + ) config_list.append(parse_host(host, config)) test_sets.append(testcase_list) config = None suite_list.extend(suite) override_config_body = None # 同步运行用例 - summary, _ = loader.debug_suite(test_sets, project, suite_list, config_list, save=False) + summary, _ = loader.debug_suite( + test_sets, project, suite_list, config_list, save=False + ) ci_project_namespace = ser.validated_data["ci_project_namespace"] ci_project_name = ser.validated_data["ci_project_name"] ci_job_id = ser.validated_data["ci_job_id"] @@ -233,8 +257,7 @@ def run_ci_tests(self, request): return Response(ser.errors, status=status.HTTP_400_BAD_REQUEST) @swagger_auto_schema( - query_serializer=CIReportSerializer, - operation_summary="获取gitlab-ci运行的报告url", + query_serializer=CIReportSerializer, operation_summary="获取gitlab-ci运行的报告url" ) def get_ci_report_url(self, request): ser = CIReportSerializer(data=request.query_params) diff --git a/fastrunner/views/config.py b/fastrunner/views/config.py index 40c81f93..e375c664 100644 --- a/fastrunner/views/config.py +++ b/fastrunner/views/config.py @@ -1,15 +1,15 @@ from copy import deepcopy from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Q from django.utils.decorators import method_decorator -from rest_framework.response import Response +from django.db.models import Q from rest_framework.viewsets import GenericViewSet - from fastrunner import models, serializers +from rest_framework.response import Response from fastrunner.utils import response from fastrunner.utils.decorator import request_log from fastrunner.utils.parser import Format +from FasterRunner.auth import OnlyGetAuthenticator class ConfigView(GenericViewSet): @@ -17,14 +17,14 @@ class ConfigView(GenericViewSet): serializer_class = serializers.ConfigSerializer queryset = models.Config.objects - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def list(self, request): - project = request.query_params["project"] + project = request.query_params['project'] search = request.query_params["search"] - queryset = self.get_queryset().filter(project__id=project).order_by("-update_time") + queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') - if search != "": + if search != '': queryset = queryset.filter(name__contains=search) pagination_queryset = self.paginate_queryset(queryset) @@ -32,34 +32,30 @@ def list(self, request): return self.get_paginated_response(serializer.data) - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def all(self, request, **kwargs): """ get all config """ pk = kwargs["pk"] - queryset = ( - self.get_queryset() - .filter(project__id=pk) - .order_by("-update_time") - .values("id", "name", "is_default", "base_url") - ) + queryset = self.get_queryset().filter(project__id=pk). \ + order_by('-update_time').values("id", "name", "is_default", "base_url") return Response(queryset) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add(self, request): """ - add new config - { - name: str - project: int - body: dict - } + add new config + { + name: str + project: int + body: dict + } """ - config = Format(request.data, level="config") + config = Format(request.data, level='config') config.parse() try: @@ -74,13 +70,13 @@ def add(self, request): "name": config.name, "base_url": config.base_url, "body": config.testcase, - "project": config.project, + "project": config.project } models.Config.objects.create(**config_body, creator=request.user.username) return Response(response.CONFIG_ADD_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def update(self, request, **kwargs): """ pk: int @@ -93,7 +89,7 @@ def update(self, request, **kwargs): } } """ - pk = kwargs["pk"] + pk = kwargs['pk'] try: config = models.Config.objects.get(id=pk) @@ -125,7 +121,7 @@ def update(self, request, **kwargs): return Response(response.CONFIG_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def copy(self, request, **kwargs): """ pk: int @@ -133,7 +129,7 @@ def copy(self, request, **kwargs): name: str } """ - pk = kwargs["pk"] + pk = kwargs['pk'] try: config = models.Config.objects.get(id=pk) except ObjectDoesNotExist: @@ -145,9 +141,9 @@ def copy(self, request, **kwargs): config.id = None config.is_default = False body = eval(config.body) - name = request.data["name"] + name = request.data['name'] - body["name"] = name + body['name'] = name config.name = name config.body = body config.creator = request.user.username @@ -156,7 +152,7 @@ def copy(self, request, **kwargs): return Response(response.CONFIG_ADD_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def delete(self, request, **kwargs): """ 删除一个配置 pk @@ -167,15 +163,15 @@ def delete(self, request, **kwargs): """ try: - if kwargs.get("pk"): # 单个删除 - config_obj = models.Config.objects.get(id=kwargs["pk"]) + if kwargs.get('pk'): # 单个删除 + config_obj = models.Config.objects.get(id=kwargs['pk']) if models.CaseStep.objects.filter(method="config", name=config_obj.name).exists(): return Response(response.CONFIG_IS_USED) config_obj.delete() else: delete_item = 0 for content in request.data: - config_obj = models.Config.objects.get(id=content["id"]) + config_obj = models.Config.objects.get(id=content['id']) if models.CaseStep.objects.filter(method="config", name=config_obj.name).exists(): continue else: @@ -194,16 +190,18 @@ class VariablesView(GenericViewSet): serializer_class = serializers.VariablesSerializer queryset = models.Variables.objects - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def list(self, request): - project = request.query_params["project"] + project = request.query_params['project'] search = request.query_params["search"] - queryset = self.get_queryset().filter(project__id=project).order_by("-update_time") + queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') - if search != "": + if search != '': queryset = queryset.filter( - Q(key__contains=search) | Q(value__contains=search) | Q(description__contains=search) + Q(key__contains=search) | + Q(value__contains=search) | + Q(description__contains=search) ) pagination_queryset = self.paginate_queryset(queryset) @@ -211,15 +209,15 @@ def list(self, request): return self.get_paginated_response(serializer.data) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add(self, request): """ - add new variables - { - key: str - value: str - project: int - } + add new variables + { + key: str + value: str + project: int + } """ ser = self.serializer_class(data=request.data) if ser.is_valid(): @@ -237,10 +235,10 @@ def add(self, request): return Response(response.CONFIG_ADD_SUCCESS) else: res = deepcopy(response.PROJECT_NOT_EXISTS) - res["msg"] = ser.errors + res['msg'] = ser.errors return Response(res) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def update(self, request, **kwargs): """ pk: int @@ -250,7 +248,7 @@ def update(self, request, **kwargs): } """ - project_id = kwargs["pk"] + project_id = kwargs['pk'] variable_id = request.data["id"] try: variables = models.Variables.objects.get(id=variable_id) @@ -258,11 +256,10 @@ def update(self, request, **kwargs): except ObjectDoesNotExist: return Response(response.VARIABLES_NOT_EXISTS) - if ( - models.Variables.objects.exclude(id=variable_id) - .filter(project_id=project_id, key=request.data["key"]) - .first() - ): + if models.Variables.objects.exclude(id=variable_id).filter( + project_id=project_id, + key=request.data['key'] + ).first(): return Response(response.VARIABLES_EXISTS) variables.key = request.data["key"] @@ -272,7 +269,7 @@ def update(self, request, **kwargs): return Response(response.VARIABLES_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def delete(self, request, **kwargs): """ 删除一个变量 pk @@ -283,11 +280,11 @@ def delete(self, request, **kwargs): """ try: - if kwargs.get("pk"): # 单个删除 - models.Variables.objects.get(id=kwargs["pk"]).delete() + if kwargs.get('pk'): # 单个删除 + models.Variables.objects.get(id=kwargs['pk']).delete() else: for content in request.data: - models.Variables.objects.get(id=content["id"]).delete() + models.Variables.objects.get(id=content['id']).delete() except ObjectDoesNotExist: return Response(response.VARIABLES_NOT_EXISTS) @@ -299,24 +296,24 @@ class HostIPView(GenericViewSet): serializer_class = serializers.HostIPSerializer queryset = models.HostIP.objects - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def list(self, request): - project = request.query_params["project"] - queryset = self.get_queryset().filter(project__id=project).order_by("-update_time") + project = request.query_params['project'] + queryset = self.get_queryset().filter(project__id=project).order_by('-update_time') pagination_queryset = self.paginate_queryset(queryset) serializer = self.get_serializer(pagination_queryset, many=True) return self.get_paginated_response(serializer.data) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def add(self, request): """ - add new variables - { - name: str - value: str - project: int - } + add new variables + { + name: str + value: str + project: int + } """ try: @@ -332,14 +329,14 @@ def add(self, request): models.HostIP.objects.create(**request.data) return Response(response.HOSTIP_ADD_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def update(self, request, **kwargs): """pk: int{ name: str value:str } """ - pk = kwargs["pk"] + pk = kwargs['pk'] try: host = models.HostIP.objects.get(id=pk) @@ -347,7 +344,7 @@ def update(self, request, **kwargs): except ObjectDoesNotExist: return Response(response.HOSTIP_NOT_EXISTS) - if models.HostIP.objects.exclude(id=pk).filter(name=request.data["name"]).first(): + if models.HostIP.objects.exclude(id=pk).filter(name=request.data['name']).first(): return Response(response.HOSTIP_EXISTS) host.name = request.data["name"] @@ -356,23 +353,25 @@ def update(self, request, **kwargs): return Response(response.HOSTIP_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def delete(self, request, **kwargs): - """删除host""" + """删除host + """ try: - models.HostIP.objects.get(id=kwargs["pk"]).delete() + models.HostIP.objects.get(id=kwargs['pk']).delete() except ObjectDoesNotExist: return Response(response.HOSTIP_NOT_EXISTS) return Response(response.HOST_DEL_SUCCESS) - @method_decorator(request_log(level="DEBUG")) + @method_decorator(request_log(level='DEBUG')) def all(self, request, **kwargs): """ get all config """ pk = kwargs["pk"] - queryset = self.get_queryset().filter(project__id=pk).order_by("-update_time").values("id", "name") + queryset = self.get_queryset().filter(project__id=pk). \ + order_by('-update_time').values("id", "name") return Response(queryset) diff --git a/fastrunner/views/project.py b/fastrunner/views/project.py index 1ac1f5f4..97f97860 100644 --- a/fastrunner/views/project.py +++ b/fastrunner/views/project.py @@ -7,6 +7,7 @@ from rest_framework.viewsets import GenericViewSet from FasterRunner import pagination +from FasterRunner.auth import OnlyGetAuthenticator from fastrunner import models, serializers from fastrunner.dto.tree_dto import TreeOut, TreeUniqueIn, TreeUpdateIn from fastrunner.services.tree_service_impl import tree_service @@ -23,11 +24,7 @@ class ProjectView(GenericViewSet): 项目增删改查 """ - queryset = ( - models.Project.objects.filter(is_deleted=0) - .all() - .order_by("-update_time") - ) + queryset = models.Project.objects.filter(is_deleted=0).all().order_by("-update_time") serializer_class = serializers.ProjectSerializer pagination_class = pagination.MyCursorPagination @@ -76,9 +73,7 @@ def update(self, request): return Response(response.SYSTEM_ERROR) if request.data["name"] != project.name: - if models.Project.objects.filter( - name=request.data["name"] - ).first(): + if models.Project.objects.filter(name=request.data["name"]).first(): return Response(response.PROJECT_EXISTS) # 调用save方法update_time字段才会自动更新 @@ -104,7 +99,7 @@ def delete(self, request): project = models.Project.objects.get(id=request.data["id"]) # 改完软删除 project.is_deleted = 1 - project.save(update_fields=["is_deleted"]) + project.save(update_fields=['is_deleted']) # prepare.project_end(project) return Response(response.PROJECT_DELETE_SUCCESS) @@ -126,9 +121,7 @@ def single(self, request, **kwargs): serializer = self.get_serializer(queryset, many=False) project_info = prepare.get_project_detail_v2(pk) - jira_core_case_cover_rate: dict = ( - prepare.get_jira_core_case_cover_rate(pk) - ) + jira_core_case_cover_rate: dict = prepare.get_jira_core_case_cover_rate(pk) project_info.update(jira_core_case_cover_rate) project_info.update(serializer.data) @@ -150,15 +143,9 @@ class DashBoardView(GenericViewSet): def get(self, request): _, report_status = prepare.aggregate_reports_by_status(0) _, report_type = prepare.aggregate_reports_by_type(0) - report_day = prepare.aggregate_reports_or_case_bydate( - "day", models.Report - ) - report_week = prepare.aggregate_reports_or_case_bydate( - "week", models.Report - ) - report_month = prepare.aggregate_reports_or_case_bydate( - "month", models.Report - ) + report_day = prepare.aggregate_reports_or_case_bydate("day", models.Report) + report_week = prepare.aggregate_reports_or_case_bydate("week", models.Report) + report_month = prepare.aggregate_reports_or_case_bydate("month", models.Report) api_day = prepare.aggregate_apis_bydate("day") api_week = prepare.aggregate_apis_bydate("week") @@ -169,12 +156,8 @@ def get(self, request): yapi_month = prepare.aggregate_apis_bydate("month", True) case_day = prepare.aggregate_reports_or_case_bydate("day", models.Case) - case_week = prepare.aggregate_reports_or_case_bydate( - "week", models.Case - ) - case_month = prepare.aggregate_reports_or_case_bydate( - "month", models.Case - ) + case_week = prepare.aggregate_reports_or_case_bydate("week", models.Case) + case_month = prepare.aggregate_reports_or_case_bydate("month", models.Case) res = { "report": { @@ -261,9 +244,7 @@ def get(self, request, **kwargs): """ resp: StandResponse[TreeOut] = tree_service.get_or_create( - TreeUniqueIn( - project_id=kwargs.pop("pk"), type=request.query_params["type"] - ) + TreeUniqueIn(project_id=kwargs.pop("pk"), type=request.query_params["type"]) ) return Response(resp.dict()) @@ -272,9 +253,7 @@ def patch(self, request, **kwargs): """ 修改树形结构,ID不能重复 """ - res = tree_service.patch( - pk=kwargs["pk"], payload=TreeUpdateIn(**request.data) - ) + res = tree_service.patch(pk=kwargs["pk"], payload=TreeUpdateIn(**request.data)) return Response(res.dict()) try: body = request.data["body"] @@ -313,12 +292,9 @@ def list(self, request): count_data = ( self.get_queryset() .filter( - project=project, - create_time__range=(day.get_day(-5), day.get_day()), - ) - .extra( - select={"create_time": "DATE_FORMAT(create_time,'%%m-%%d')"} + project=project, create_time__range=(day.get_day(-5), day.get_day()) ) + .extra(select={"create_time": "DATE_FORMAT(create_time,'%%m-%%d')"}) .values("create_time") .annotate(counts=Count("id")) .values("create_time", "counts") @@ -329,9 +305,7 @@ def list(self, request): } report_count = [create_time_report_map.get(d, 0) for d in recent7days] - return Response( - {"recent7days": recent7days, "report_count": report_count} - ) + return Response({"recent7days": recent7days, "report_count": report_count}) # diff --git a/fastrunner/views/report.py b/fastrunner/views/report.py index 48b28476..7681f819 100644 --- a/fastrunner/views/report.py +++ b/fastrunner/views/report.py @@ -7,16 +7,15 @@ from django.utils.decorators import method_decorator from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet - from FasterRunner import pagination from fastrunner import models, serializers -from fastrunner.utils import convert2hrp, response -from fastrunner.utils.convert2boomer import Boomer, BoomerExtendCmd +from fastrunner.utils import response, convert2hrp from fastrunner.utils.convert2hrp import Hrp +from fastrunner.utils.convert2boomer import Boomer, BoomerExtendCmd from fastrunner.utils.decorator import request_log -class ConvertRequest: +class ConvertRequest(object): @classmethod def _to_curl(cls, request, compressed=False, verify=True): """ @@ -36,7 +35,7 @@ def _to_curl(cls, request, compressed=False, verify=True): ] for k, v in sorted(request.headers.items()): - parts += [("-H", f"{k}: {v}")] + parts += [("-H", "{0}: {1}".format(k, v))] if request.body: body = request.body @@ -65,7 +64,7 @@ def _to_curl(cls, request, compressed=False, verify=True): @classmethod def _make_fake_req(cls, request_meta_dict): - class RequestMeta: + class RequestMeta(object): ... req = RequestMeta() @@ -118,7 +117,10 @@ def get_authenticators(self): # 查看报告详情不需要鉴权 # self.request.path = '/api/fastrunner/reports/3053/' pattern = re.compile(r"/api/fastrunner/reports/\d+/") - if self.request.method == "GET" and re.search(pattern, self.request.path) is not None: + if ( + self.request.method == "GET" + and re.search(pattern, self.request.path) is not None + ): return [] return super().get_authenticators() @@ -132,7 +134,9 @@ def list(self, request): report_status = request.query_params["reportStatus"] only_me = request.query_params["onlyMe"] - queryset = self.get_queryset().filter(project__id=project).order_by("-update_time") + queryset = ( + self.get_queryset().filter(project__id=project).order_by("-update_time") + ) # 前端传过来是小写的字符串,不是python的True if only_me == "true": diff --git a/fastrunner/views/run.py b/fastrunner/views/run.py index dafaa8b4..7abbaf24 100644 --- a/fastrunner/views/run.py +++ b/fastrunner/views/run.py @@ -1,102 +1,101 @@ +import json # from loguru import logger -import datetime import logging +import datetime from django.core.exceptions import ObjectDoesNotExist -from rest_framework.decorators import api_view -from rest_framework.response import Response - -from fastrunner import models, tasks +from rest_framework.decorators import api_view, authentication_classes from fastrunner.utils import loader, response +from fastrunner import tasks +from rest_framework.response import Response from fastrunner.utils.decorator import request_log +from fastrunner.utils.ding_message import DingMessage from fastrunner.utils.host import parse_host from fastrunner.utils.parser import Format +from fastrunner import models logger = logging.getLogger(__name__) """运行方式 """ -config_err = {"success": False, "msg": "指定的配置文件不存在", "code": "9999"} +config_err = { + "success": False, + "msg": "指定的配置文件不存在", + "code": "9999" +} -@api_view(["POST"]) -@request_log(level="INFO") +@api_view(['POST']) +@request_log(level='INFO') def run_api(request): - """run api by body""" - name = request.data.pop("config") + """ run api by body + """ + name = request.data.pop('config') host = request.data.pop("host") api = Format(request.data) api.parse() config = None - if name != "请选择": + if name != '请选择': try: config = eval(models.Config.objects.get(name=name, project__id=api.project).body) except ObjectDoesNotExist: - logger.error(f"指定配置文件不存在:{name}") + logger.error("指定配置文件不存在:{name}".format(name=name)) return Response(config_err) if host != "请选择": host = models.HostIP.objects.get(name=host, project__id=api.project).value.splitlines() api.testcase = parse_host(host, api.testcase) - summary = loader.debug_api( - api.testcase, - api.project, - name=api.name, - config=parse_host(host, config), - user=request.user, - ) + summary = loader.debug_api(api.testcase, api.project, name=api.name, config=parse_host(host, config), + user=request.user) return Response(summary) -@api_view(["GET"]) -@request_log(level="INFO") +@api_view(['GET']) +@request_log(level='INFO') def run_api_pk(request, **kwargs): - """run api by pk and config""" + """run api by pk and config + """ host = request.query_params["host"] - api = models.API.objects.get(id=kwargs["pk"]) + api = models.API.objects.get(id=kwargs['pk']) name = request.query_params["config"] - config = None if name == "请选择" else eval(models.Config.objects.get(name=name, project=api.project).body) + config = None if name == '请选择' else eval(models.Config.objects.get(name=name, project=api.project).body) test_case = eval(api.body) if host != "请选择": host = models.HostIP.objects.get(name=host, project=api.project).value.splitlines() test_case = parse_host(host, test_case) - summary = loader.debug_api( - test_case, - api.project.id, - name=api.name, - config=parse_host(host, config), - user=request.user, - ) + summary = loader.debug_api(test_case, api.project.id, name=api.name, config=parse_host(host, config), + user=request.user) return Response(summary) def auto_run_api_pk(**kwargs): - """run api by pk and config""" - id = kwargs["id"] - env = kwargs["config"] - config_name = "rig_prod" if env == 1 else "rig_test" + """run api by pk and config + """ + id = kwargs['id'] + env = kwargs['config'] + config_name = 'rig_prod' if env == 1 else 'rig_test' api = models.API.objects.get(id=id) config = eval(models.Config.objects.get(name=config_name, project=api.project).body) test_case = eval(api.body) summary = loader.debug_api(test_case, api.project.id, config=config) - api_request = summary["details"][0]["records"][0]["meta_data"]["request"] - api_response = summary["details"][0]["records"][0]["meta_data"]["response"] + api_request = summary['details'][0]['records'][0]['meta_data']['request'] + api_response = summary['details'][0]['records'][0]['meta_data']['response'] # API执行成功,设置tag为自动运行成功 - if summary["stat"]["failures"] == 0 and summary["stat"]["errors"] == 0: + if summary['stat']['failures'] == 0 and summary['stat']['errors'] == 0: models.API.objects.filter(id=id).update(tag=3) - return "success" - elif summary["stat"]["failures"] == 1: + return 'success' + elif summary['stat']['failures'] == 1: # models.API.objects.filter(id=id).update(tag=2) - return "fail" + return 'fail' def update_auto_case_step(**kwargs): @@ -117,19 +116,19 @@ def update_auto_case_step(**kwargs): :return: """ # 去掉多余字段 - kwargs.pop("project") - kwargs.pop("rig_id") - kwargs.pop("relation") + kwargs.pop('project') + kwargs.pop('rig_id') + kwargs.pop('relation') # 测试环境0,对应97 生产环境1,对应98 - rig_env = kwargs.pop("rig_env") + rig_env = kwargs.pop('rig_env') case_id = 98 if rig_env == 1 else 97 # 获取case的长度,+1是因为增加了一个case_step, length = models.Case.objects.filter(id=case_id).first().length + 1 # case的长度也就是case_step的数量 - kwargs["step"] = length - kwargs["case_id"] = case_id - case_step_name = kwargs["name"] + kwargs['step'] = length + kwargs['case_id'] = case_id + case_step_name = kwargs['name'] # api不存在用例中,就新增,已经存在就更新 is_case_step_name = models.CaseStep.objects.filter(case_id=case_id).filter(name=case_step_name) if len(is_case_step_name) == 0: @@ -139,8 +138,8 @@ def update_auto_case_step(**kwargs): is_case_step_name.update(update_time=datetime.datetime.now(), **kwargs) -@api_view(["POST"]) -@request_log(level="INFO") +@api_view(['POST']) +@request_log(level='INFO') def run_api_tree(request): """run api by tree { @@ -153,24 +152,23 @@ def run_api_tree(request): """ # order by id default host = request.data["host"] - project = request.data["project"] + project = request.data['project'] relation = request.data["relation"] back_async = request.data["async"] name = request.data["name"] config = request.data["config"] - config = None if config == "请选择" else eval(models.Config.objects.get(name=config, project__id=project).body) + config = None if config == '请选择' else eval(models.Config.objects.get(name=config, project__id=project).body) test_case = [] if host != "请选择": host = models.HostIP.objects.get(name=host, project=project).value.splitlines() for relation_id in relation: - api = ( - models.API.objects.filter(project__id=project, relation=relation_id, delete=0).order_by("id").values("body") - ) + api = models.API.objects.filter(project__id=project, relation=relation_id, delete=0).order_by('id').values( + 'body') for content in api: - api = eval(content["body"]) + api = eval(content['body']) test_case.append(parse_host(host, api)) if back_async: @@ -178,19 +176,14 @@ def run_api_tree(request): summary = loader.TEST_NOT_EXISTS summary["msg"] = "接口运行中,请稍后查看报告" else: - summary = loader.debug_api( - test_case, - project, - name=f"批量运行{len(test_case)}条API", - config=parse_host(host, config), - user=request.user, - ) + summary = loader.debug_api(test_case, project, name=f'批量运行{len(test_case)}条API', + config=parse_host(host, config), user=request.user) return Response(summary) @api_view(["POST"]) -@request_log(level="INFO") +@request_log(level='INFO') def run_testsuite(request): """debug testsuite { @@ -218,30 +211,25 @@ def run_testsuite(request): test_case.append(parse_host(host, test)) - summary = loader.debug_api( - test_case, - project, - name=name, - config=parse_host(host, config), - user=request.user, - ) + summary = loader.debug_api(test_case, project, name=name, config=parse_host(host, config), user=request.user) return Response(summary) @api_view(["GET"]) -@request_log(level="INFO") +@request_log(level='INFO') def run_testsuite_pk(request, **kwargs): """run testsuite by pk - { - project: int, - name: str, - host: str - } + { + project: int, + name: str, + host: str + } """ pk = kwargs["pk"] - test_list = models.CaseStep.objects.filter(case__id=pk).order_by("step").values("body") + test_list = models.CaseStep.objects. \ + filter(case__id=pk).order_by("step").values("body") project = request.query_params["project"] name = request.query_params["name"] @@ -268,19 +256,13 @@ def run_testsuite_pk(request, **kwargs): summary = response.TASK_RUN_SUCCESS else: - summary = loader.debug_api( - test_case, - project, - name=name, - config=parse_host(host, config), - user=request.user, - ) + summary = loader.debug_api(test_case, project, name=name, config=parse_host(host, config), user=request.user) return Response(summary) -@api_view(["POST"]) -@request_log(level="INFO") +@api_view(['POST']) +@request_log(level='INFO') def run_suite_tree(request): """run suite by tree { @@ -292,7 +274,7 @@ def run_suite_tree(request): } """ # order by id default - project = request.data["project"] + project = request.data['project'] relation = request.data["relation"] back_async = request.data["async"] report = request.data["name"] @@ -310,18 +292,19 @@ def run_suite_tree(request): suite_list = [] config_list = [] for relation_id in relation: - case_name_id_mapping_list = list( - models.Case.objects.filter(project__id=project, relation=relation_id).order_by("id").values("id", "name") - ) + case_name_id_mapping_list = list(models.Case.objects.filter(project__id=project, + relation=relation_id).order_by('id').values('id', + 'name')) for content in case_name_id_mapping_list: - test_list = models.CaseStep.objects.filter(case__id=content["id"]).order_by("step").values("body") + test_list = models.CaseStep.objects. \ + filter(case__id=content["id"]).order_by("step").values("body") testcase_list = [] for content in test_list: body = eval(content["body"]) - if body["request"].get("url"): + if body["request"].get('url'): testcase_list.append(parse_host(host, body)) - elif config is None and body["request"].get("base_url"): + elif config is None and body["request"].get('base_url'): config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) config_list.append(parse_host(host, config)) test_sets.append(testcase_list) @@ -333,20 +316,13 @@ def run_suite_tree(request): summary = loader.TEST_NOT_EXISTS summary["msg"] = "用例运行中,请稍后查看报告" else: - summary, _ = loader.debug_suite( - test_sets, - project, - suite_list, - config_list, - save=True, - user=request.user, - ) + summary, _ = loader.debug_suite(test_sets, project, suite_list, config_list, save=True, user=request.user) return Response(summary) -@api_view(["POST"]) -@request_log(level="INFO") +@api_view(['POST']) +@request_log(level='INFO') def run_multi_tests(request): """ 通过指定id,运行多个指定用例 @@ -362,7 +338,7 @@ def run_multi_tests(request): ] } """ - project = request.data["project"] + project = request.data['project'] report_name = request.data["name"] # 默认同步运行用例 back_async = request.data.get("async") or False @@ -370,23 +346,22 @@ def run_multi_tests(request): config_body_mapping = {} # 解析用例列表中的配置 for config in case_config_mapping_list: - config_name = config["config_name"] + config_name = config['config_name'] if not config_body_mapping.get(config_name): config_body_mapping[config_name] = eval( - models.Config.objects.get(name=config["config_name"], project__id=project).body - ) + models.Config.objects.get(name=config['config_name'], project__id=project).body) test_sets = [] suite_list = [] config_list = [] for case_config_mapping in case_config_mapping_list: - case_id = case_config_mapping["id"] - config_name = case_config_mapping["config_name"] + case_id = case_config_mapping['id'] + config_name = case_config_mapping['config_name'] # 获取用例的所有步骤 case_step_list = models.CaseStep.objects.filter(case__id=case_id).order_by("step").values("body") parsed_case_step_list = [] for case_step in case_step_list: - body = eval(case_step["body"]) - if body["request"].get("url"): + body = eval(case_step['body']) + if body["request"].get('url'): parsed_case_step_list.append(body) config_body = config_body_mapping[config_name] # 记录当前用例的配置信息 @@ -408,14 +383,14 @@ def run_multi_tests(request): config_list, save=True, user=request.user, - report_name=report_name, + report_name=report_name ) return Response(summary) @api_view(["POST"]) -@request_log(level="INFO") +@request_log(level='INFO') def run_test(request): """debug single test { @@ -438,11 +413,14 @@ def run_test(request): config = eval(models.Config.objects.get(project=project, name=config["name"]).body) summary = loader.debug_api( - parse_host(host, loader.load_test(body)), + parse_host( + host, + loader.load_test(body) + ), project, - name=body.get("name", None), + name=body.get('name', None), config=parse_host(host, config), - user=request.user, + user=request.user ) return Response(summary) diff --git a/fastrunner/views/run_all_auto_case.py b/fastrunner/views/run_all_auto_case.py index ee751228..6510d911 100644 --- a/fastrunner/views/run_all_auto_case.py +++ b/fastrunner/views/run_all_auto_case.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 # @File: run_all_auto_case.py @@ -6,14 +7,15 @@ # @Email: lihuacai168@gmail.com # @Software: PyCharm +from django.http import HttpResponse, JsonResponse +from django.conf import settings import json -from django.conf import settings -from django.http import HttpResponse, JsonResponse from django_celery_beat.models import PeriodicTask -from FasterRunner.mycelery import app from fastrunner import models +from FasterRunner.mycelery import app + from fastrunner.utils.host import parse_host @@ -44,7 +46,11 @@ def build_testsuite(project: int, config_id: int = 0, testcase_tag: int = 1): """ 组装可运行的用例 """ - cases = list(models.Case.objects.filter(project__id=project, tag=testcase_tag).order_by("id").values("id", "name")) + cases = list( + models.Case.objects.filter(project__id=project, tag=testcase_tag) + .order_by("id") + .values("id", "name") + ) test_sets = [] config_list = [] host = "请选择" @@ -53,7 +59,11 @@ def build_testsuite(project: int, config_id: int = 0, testcase_tag: int = 1): # 前端有指定config,会覆盖用例本身的config reload_config = eval(models.Config.objects.get(id=config_id).body) for case in cases: - test_list = models.CaseStep.objects.filter(case__id=case["id"]).order_by("step").values("body") + test_list = ( + models.CaseStep.objects.filter(case__id=case["id"]) + .order_by("step") + .values("body") + ) testcase_list = [] for content in test_list: body = eval(content["body"]) @@ -61,7 +71,11 @@ def build_testsuite(project: int, config_id: int = 0, testcase_tag: int = 1): testcase_list.append(parse_host(host, body)) elif body["request"].get("base_url"): if reload_config is None: - config = eval(models.Config.objects.get(name=body["name"], project__id=project).body) + config = eval( + models.Config.objects.get( + name=body["name"], project__id=project + ).body + ) else: config = reload_config config_list.append(parse_host(host, config)) diff --git a/fastrunner/views/schedule.py b/fastrunner/views/schedule.py index 901157ca..4699e759 100644 --- a/fastrunner/views/schedule.py +++ b/fastrunner/views/schedule.py @@ -1,17 +1,17 @@ import json import croniter + from django.utils.decorators import method_decorator from django_celery_beat import models -from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet - +from rest_framework.response import Response from FasterRunner import pagination -from FasterRunner.mycelery import app from fastrunner import serializers from fastrunner.utils import response from fastrunner.utils.decorator import request_log from fastrunner.utils.task import Task +from FasterRunner.mycelery import app class ScheduleView(GenericViewSet): @@ -28,11 +28,7 @@ def check_crontab_expr(expr: str) -> bool: try: # check crontab croniter.croniter(expr) - except ( - croniter.CroniterNotAlphaError, - croniter.CroniterBadCronError, - croniter.CroniterBadDateError, - ): + except (croniter.CroniterNotAlphaError, croniter.CroniterBadCronError, croniter.CroniterBadDateError): return False return True @@ -44,7 +40,9 @@ def list(self, request): project = request.query_params.get("project") task_name = request.query_params.get("task_name") creator = request.query_params.get("creator") - schedule = self.get_queryset().filter(description=project).order_by("-date_changed") + schedule = ( + self.get_queryset().filter(description=project).order_by("-date_changed") + ) if task_name: schedule = schedule.filter(name__contains=task_name) if creator: @@ -65,17 +63,11 @@ def add(self, request): copy: str project: int } - """ + """ ser = serializers.ScheduleDeSerializer(data=request.data) if ser.is_valid(): if not self.check_crontab_expr(request.data.get("crontab")): - return Response( - { - "code": "0101", - "success": False, - "msg": f"{request.data.get('crontab')}, 不合法的定时任务表达式", - } - ) + return Response({"code": "0101", "success": False, "msg": f"{request.data.get('crontab')}, 不合法的定时任务表达式"}) request.data.update({"creator": request.user.username}) task = Task(**request.data) resp = task.add_task() @@ -109,13 +101,7 @@ def update(self, request, **kwargs): ser = serializers.ScheduleDeSerializer(data=request.data) if ser.is_valid(): if not self.check_crontab_expr(request.data.get("crontab")): - return Response( - { - "code": "0101", - "success": False, - "msg": f"{request.data.get('crontab')}, 不合法的定时任务表达式", - } - ) + return Response({"code": "0101", "success": False, "msg": f"{request.data.get('crontab')}, 不合法的定时任务表达式"}) task = Task(**request.data) resp = task.update_task(kwargs["pk"]) return Response(resp) diff --git a/fastrunner/views/suite.py b/fastrunner/views/suite.py index b0c97d2e..42cf6b86 100644 --- a/fastrunner/views/suite.py +++ b/fastrunner/views/suite.py @@ -5,12 +5,13 @@ from django.db.models import Q from django.utils.decorators import method_decorator from rest_framework import status -from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet - from fastrunner import models, serializers -from fastrunner.utils import prepare, response + +from rest_framework.response import Response +from fastrunner.utils import response +from fastrunner.utils import prepare from fastrunner.utils.decorator import request_log @@ -30,12 +31,12 @@ def case_step_search(search): 搜索case_step的url或者name 返回对应的case_id """ - case_id = models.CaseStep.objects.filter(Q(name__contains=search) | Q(url__contains=search)).values("case_id") + case_id = models.CaseStep.objects.filter(Q(name__contains=search) | Q(url__contains=search)).values('case_id') - case_id = set([item["case_id"] for _, item in enumerate(case_id)]) + case_id = set([item['case_id'] for _, item in enumerate(case_id)]) return case_id - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def get(self, request): """ 查询指定CASE列表,不包含CASE STEP @@ -54,23 +55,23 @@ def get(self, request): only_me = ser.validated_data.get("onlyMe") # update_time 降序排列 - queryset = self.get_queryset().filter(project__id=project).order_by("-create_time") + queryset = self.get_queryset().filter(project__id=project).order_by('-create_time') if only_me is True: queryset = queryset.filter(creator=request.user) - if node != "": + if node != '': queryset = queryset.filter(relation=node) - if case_type != "": + if case_type != '': queryset = queryset.filter(tag=case_type) - if search != "": + if search != '': # 用例名称搜索 - if search_type == "1": + if search_type == '1': queryset = queryset.filter(name__contains=search) # API名称或者API URL搜索 - elif search_type == "2": + elif search_type == '2': case_id = self.case_step_search(search) queryset = queryset.filter(pk__in=case_id) @@ -81,7 +82,7 @@ def get(self, request): else: return Response(ser.errors, status=status.HTTP_400_BAD_REQUEST) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def copy(self, request, **kwargs): """ pk int: test id @@ -91,10 +92,10 @@ def copy(self, request, **kwargs): project: int } """ - pk = kwargs["pk"] - name = request.data["name"] + pk = kwargs['pk'] + name = request.data['name'] username = request.user.username - if "|" in name: + if '|' in name: resp = self.split(pk, name) else: case = models.Case.objects.get(id=pk) @@ -117,8 +118,8 @@ def copy(self, request, **kwargs): return Response(resp) def split(self, pk, name): - split_case_name = name.split("|")[0] - split_condition = name.split("|")[1] + split_case_name = name.split('|')[0] + split_condition = name.split('|')[1] # 更新原本的case长度 case = models.Case.objects.get(id=pk) @@ -145,7 +146,7 @@ def split(self, pk, name): # case_step.filter(name=).update_or_create(defaults={'case_id': case.id}) return response.CASE_SPILT_SUCCESS - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def patch(self, request, **kwargs): """ 更新测试用例集 @@ -157,12 +158,12 @@ def patch(self, request, **kwargs): } """ - pk = kwargs["pk"] + pk = kwargs['pk'] project = request.data.pop("project") - body = request.data.pop("body") + body = request.data.pop('body') relation = request.data.pop("relation") - is_exist_case_obj = models.Case.objects.exclude(id=pk).filter(name=request.data["name"], project__id=project) + is_exist_case_obj = models.Case.objects.exclude(id=pk).filter(name=request.data['name'], project__id=project) if relation: # 兼容新建用例时,保存用例步骤就提交修改 is_exist_case_obj = is_exist_case_obj.filter(relation=relation) @@ -173,29 +174,32 @@ def patch(self, request, **kwargs): prepare.update_casestep(body, case, username=request.user.username) - request.data["tag"] = self.tag_options[request.data["tag"]] + request.data['tag'] = self.tag_options[request.data['tag']] models.Case.objects.filter(id=pk).update( update_time=datetime.datetime.now(), updater=request.user.username, - **request.data, + **request.data ) return Response(response.CASE_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def move(self, request): - project: int = request.data.get("project") - relation: int = request.data.get("relation") - cases: list = request.data.get("case") - ids = [case["id"] for case in cases] + project: int = request.data.get('project') + relation: int = request.data.get('relation') + cases: list = request.data.get('case') + ids = [case['id'] for case in cases] try: - models.Case.objects.filter(project=project, id__in=ids).update(relation=relation) + models.Case.objects.filter( + project=project, + id__in=ids + ).update(relation=relation) except ObjectDoesNotExist: return Response(response.CASE_NOT_EXISTS) return Response(response.CASE_UPDATE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def post(self, request): """ 新增测试用例集 @@ -216,8 +220,8 @@ def post(self, request): """ try: - pk = request.data["project"] - request.data["project"] = models.Project.objects.get(id=pk) + pk = request.data['project'] + request.data['project'] = models.Project.objects.get(id=pk) except KeyError: return Response(response.KEY_MISS) @@ -225,9 +229,9 @@ def post(self, request): except ObjectDoesNotExist: return Response(response.PROJECT_NOT_EXISTS) - body = request.data.pop("body") + body = request.data.pop('body') - request.data["tag"] = self.tag_options[request.data["tag"]] + request.data['tag'] = self.tag_options[request.data['tag']] with transaction.atomic(): save_point = transaction.savepoint() case = models.Case.objects.create(**request.data, creator=request.user.username) @@ -245,17 +249,17 @@ def post(self, request): # apis = models.API.objects.filter(pk__in=api_ids).all() # case.apis.add(*apis) transaction.savepoint_commit(save_point) - res = {"test_id": case.id} + res = {'test_id': case.id} res.update(response.CASE_ADD_SUCCESS) return Response(res) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def delete(self, request, **kwargs): """ pk: test id delete single [{id:int}] delete batch """ - pk = kwargs.get("pk") + pk = kwargs.get('pk') try: if pk: @@ -263,7 +267,7 @@ def delete(self, request, **kwargs): else: case_ids: list = [] for content in request.data: - case_ids.append(content["id"]) + case_ids.append(content['id']) prepare.case_end(case_ids) except ObjectDoesNotExist: @@ -271,27 +275,30 @@ def delete(self, request, **kwargs): return Response(response.CASE_DELETE_SUCCESS) - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def put(self, request, **kwargs): # case_id - pk = kwargs["pk"] + pk = kwargs['pk'] # 在case_step表中找出case_id对应的所有记录,并且排除config api_id_list_of_dict = list( - models.CaseStep.objects.filter(case_id=pk).exclude(method="config").values("source_api_id", "step") - ) + models.CaseStep.objects.filter(case_id=pk).exclude(method='config').values('source_api_id', 'step')) # 通过source_api_id找到原来的api # 把原来api的name, body, url, method更新到case_step中 for item in api_id_list_of_dict: - source_api_id: int = item["source_api_id"] + source_api_id: int = item['source_api_id'] # 不存在api_id的直接跳过 if source_api_id == 0: continue - step: int = item["step"] + step: int = item['step'] source_api = models.API.objects.filter(pk=source_api_id).values("name", "body", "url", "method").first() if source_api is not None: - models.CaseStep.objects.filter(case_id=pk, source_api_id=source_api_id, step=step).update(**source_api) + models.CaseStep.objects.filter( + case_id=pk, + source_api_id=source_api_id, + step=step + ).update(**source_api) models.Case.objects.filter(pk=pk).update(update_time=datetime.datetime.now()) return Response(response.CASE_STEP_SYNC_SUCCESS) @@ -299,9 +306,9 @@ def update_tag(self, request): """ 批量更新用例类型 """ - case_ids: list = request.data.get("case_ids", []) - project_id: list = request.data.get("project_id", 0) - tag: list = request.data.get("tag") + case_ids: list = request.data.get('case_ids', []) + project_id: list = request.data.get('project_id', 0) + tag: list = request.data.get('tag') models.Case.objects.filter(project_id=project_id, pk__in=case_ids).update(tag=tag) return Response(response.CASE_UPDATE_SUCCESS) @@ -311,19 +318,19 @@ class CaseStepView(APIView): 测试用例step操作视图 """ - @method_decorator(request_log(level="INFO")) + @method_decorator(request_log(level='INFO')) def get(self, request, **kwargs): """ 返回用例集信息 """ - pk = kwargs["pk"] + pk = kwargs['pk'] - queryset = models.CaseStep.objects.filter(case__id=pk).order_by("step") + queryset = models.CaseStep.objects.filter(case__id=pk).order_by('step') serializer = serializers.CaseStepSerializer(instance=queryset, many=True) resp = { "case": serializers.CaseSerializer(instance=models.Case.objects.get(id=pk), many=False).data, - "step": serializer.data, + "step": serializer.data } return Response(resp) diff --git a/fastrunner/views/timer_task.py b/fastrunner/views/timer_task.py index 3e75a1af..d4ee4d4d 100644 --- a/fastrunner/views/timer_task.py +++ b/fastrunner/views/timer_task.py @@ -1,15 +1,16 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author:梨花菜 -# @File: timer_task.py +# @File: timer_task.py # @Time : 2018/12/29 13:44 # @Email: lihuacai168@gmail.com # @Software: PyCharm import datetime from fastrunner import models -from fastrunner.utils.ding_message import DingMessage from fastrunner.utils.loader import debug_api, save_summary +from fastrunner.utils.ding_message import DingMessage # 单个用例组 @@ -21,14 +22,15 @@ def auto_run_testsuite_pk(**kwargs): :return: """ - pk = kwargs.get("pk") - run_type = kwargs.get("run_type") - project_id = kwargs.get("project_id") + pk = kwargs.get('pk') + run_type = kwargs.get('run_type') + project_id = kwargs.get('project_id') name = models.Case.objects.get(pk=pk).name # 通过主键获取单个用例 - test_list = models.CaseStep.objects.filter(case__id=pk).order_by("step").values("body") + test_list = models.CaseStep.objects. \ + filter(case__id=pk).order_by("step").values("body") # 把用例加入列表 testcase_list = [] @@ -42,12 +44,10 @@ def auto_run_testsuite_pk(**kwargs): summary = debug_api(testcase_list, project_id, name=name, config=config, save=False) - save_summary( - f"{name}_" + datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - summary, - project_id, - type=3, - ) + save_summary(f'{name}_'+datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'), summary, project_id, type=3) ding_message = DingMessage(run_type) ding_message.send_ding_msg(summary) + + + diff --git a/fastrunner/views/yapi.py b/fastrunner/views/yapi.py index b44fa8be..5dc5ace5 100644 --- a/fastrunner/views/yapi.py +++ b/fastrunner/views/yapi.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: yapi.py @@ -7,30 +8,27 @@ # from loguru import logger import logging -from django_bulk_update.helper import bulk_update -from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.response import Response +from django_bulk_update.helper import bulk_update -from fastrunner import models -from fastrunner.utils import response from fastrunner.utils.parser import Yapi +from fastrunner.utils import response +from fastrunner import models logger = logging.getLogger(__name__) class YAPIView(APIView): + def post(self, request, **kwargs): logger.info("开始批量导入yapi接口...") - faster_project_id = kwargs["pk"] + faster_project_id = kwargs['pk'] obj = models.Project.objects.get(pk=faster_project_id) token = obj.yapi_openapi_token yapi_base_url = obj.yapi_base_url - yapi = Yapi( - yapi_base_url=yapi_base_url, - token=token, - faster_project_id=faster_project_id, - ) - imported_apis = models.API.objects.filter(project_id=faster_project_id, creator="yapi", delete=0) + yapi = Yapi(yapi_base_url=yapi_base_url, token=token, faster_project_id=faster_project_id) + imported_apis = models.API.objects.filter(project_id=faster_project_id, creator='yapi', delete=0) imported_apis_mapping = {api.yapi_id: api.ypai_up_time for api in imported_apis} create_ids, update_ids = yapi.get_create_or_update_apis(imported_apis_mapping) try: @@ -44,7 +42,7 @@ def post(self, request, **kwargs): return Response(response.YAPI_NOT_NEED_CREATE_OR_UPDATE) api_info = yapi.get_batch_api_detail(create_ids) except Exception as e: - logger.error(f"导入yapi失败: {e}") + logger.error(f'导入yapi失败: {e}') return Response(response.YAPI_ADD_FAILED) # 把yapi解析成符合faster的api格式 @@ -56,9 +54,9 @@ def post(self, request, **kwargs): created_apis_count = len(created_objs) updated_apis_count = len(update_apis) resp = { - "createdCount": created_apis_count, - "updatedCount": updated_apis_count, + "createdCount": created_apis_count, + "updatedCount": updated_apis_count, } logger.info(f"导入完成, {created_apis_count=}, {updated_apis_count=}") resp.update(response.YAPI_ADD_SUCCESS) - return Response(resp) + return Response(resp) \ No newline at end of file diff --git a/fastuser/admin.py b/fastuser/admin.py index b18ad258..d03f3cc7 100644 --- a/fastuser/admin.py +++ b/fastuser/admin.py @@ -1,54 +1,38 @@ from django.contrib import admin -from django.contrib.auth import get_user_model from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.utils.translation import gettext_lazy as _ - +from django.contrib.auth import get_user_model +from django.utils.translation import gettext, gettext_lazy as _ # Register your models here. User = get_user_model() @admin.register(User) class UserAdmin(BaseUserAdmin): - list_display = ("username", "is_active", "belong_groups") + list_display = ('username', 'is_active', 'belong_groups') # 编辑资料的时候显示的字段 fieldsets = ( - (None, {"fields": ("username", "password")}), + (None, {'fields': ('username', 'password')}), # (_('Personal info'), {'fields': ('phone', 'first_name', 'last_name', 'email')}), # (_('Permissions'), { # 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions'), # }), - ( - _("Permissions"), - { - "fields": ( - "is_active", - "is_staff", - "is_superuser", - "groups", - ), - }, - ), + (_('Permissions'), { + 'fields': ('is_active', 'is_staff', 'is_superuser', 'groups',), + }), ) # 新增用户需要填写的字段 add_fieldsets = ( - ( - None, - { - "classes": ("wide",), - "fields": ( - "username", - "password1", - "password2", - "is_active", - "is_staff", - "groups", - ), - }, - ), + (None, { + 'classes': ('wide',), + 'fields': ('username', 'password1', 'password2', 'is_active', 'is_staff', 'groups'), + }), ) - filter_horizontal = ("groups",) + filter_horizontal = ('groups',) - @admin.display(description="所属分组") + @admin.display( + description='所属分组' + ) def belong_groups(self, obj): return ", ".join([g.name for g in obj.groups.all()]) + diff --git a/fastuser/apps.py b/fastuser/apps.py index f9efa918..768b85a8 100644 --- a/fastuser/apps.py +++ b/fastuser/apps.py @@ -2,4 +2,4 @@ class UsermanagerConfig(AppConfig): - name = "fastuser" + name = 'fastuser' diff --git a/fastuser/common/response.py b/fastuser/common/response.py index b1eb2522..6f3ef018 100644 --- a/fastuser/common/response.py +++ b/fastuser/common/response.py @@ -1,25 +1,53 @@ -KEY_MISS = {"code": "0100", "success": False, "msg": "请求数据非法"} +KEY_MISS = { + "code": "0100", + "success": False, + "msg": "请求数据非法" +} REGISTER_USERNAME_EXIST = { "code": "0101", "success": False, - "msg": "用户名已被注册", + "msg": "用户名已被注册" } REGISTER_EMAIL_EXIST = { "code": "0101", "success": False, - "msg": "邮箱已被注册", + "msg": "邮箱已被注册" } -SYSTEM_ERROR = {"code": "9999", "success": False, "msg": "System Error"} +SYSTEM_ERROR = { + "code": "9999", + "success": False, + "msg": "System Error" +} -REGISTER_SUCCESS = {"code": "0001", "success": True, "msg": "register success"} +REGISTER_SUCCESS = { + "code": "0001", + "success": True, + "msg": "register success" +} -LOGIN_FAILED = {"code": "0103", "success": False, "msg": "用户名或密码错误"} +LOGIN_FAILED = { + "code": "0103", + "success": False, + "msg": "用户名或密码错误" +} -USER_NOT_EXISTS = {"code": "0104", "success": False, "msg": "该用户未注册"} +USER_NOT_EXISTS = { + "code": "0104", + "success": False, + "msg": "该用户未注册" +} -USER_BLOCKED = {"code": "0105", "success": False, "msg": "用户被禁用"} +USER_BLOCKED = { + "code": "0105", + "success": False, + "msg": "用户被禁用" +} -LOGIN_SUCCESS = {"code": "0001", "success": True, "msg": "login success"} +LOGIN_SUCCESS = { + "code": "0001", + "success": True, + "msg": "login success" +} diff --git a/fastuser/common/token.py b/fastuser/common/token.py index e7f2ad59..1576dfde 100644 --- a/fastuser/common/token.py +++ b/fastuser/common/token.py @@ -8,7 +8,8 @@ def generate_token(username): """ timestamp = str(time.time()) - token = hashlib.md5(bytes(username, encoding="utf-8")) - token.update(bytes(timestamp, encoding="utf-8")) + token = hashlib.md5(bytes(username, encoding='utf-8')) + token.update(bytes(timestamp, encoding='utf-8')) return token.hexdigest() + diff --git a/fastuser/models.py b/fastuser/models.py index 19827f24..98fd32bf 100644 --- a/fastuser/models.py +++ b/fastuser/models.py @@ -1,8 +1,8 @@ -from django.contrib.auth.models import AbstractUser from django.db import models +from django.contrib.auth.models import AbstractUser -# Create your models here. +# Create your models here. class BaseTable(models.Model): """ @@ -12,10 +12,10 @@ class BaseTable(models.Model): class Meta: abstract = True verbose_name = "公共字段表" - db_table = "base_table" + db_table = 'base_table' - create_time = models.DateTimeField("创建时间", auto_now_add=True) - update_time = models.DateTimeField("更新时间", auto_now=True) + create_time = models.DateTimeField('创建时间', auto_now_add=True) + update_time = models.DateTimeField('更新时间', auto_now=True) creator = models.CharField(verbose_name="创建人", max_length=20, null=True) updater = models.CharField(verbose_name="更新人", max_length=20, null=True) @@ -30,13 +30,13 @@ class Meta: db_table = "user_info" level_type = ( - (0, "普通用户"), - (1, "管理员"), + (0, '普通用户'), + (1, '管理员'), ) - username = models.CharField("用户名", max_length=20, unique=True, null=False) - password = models.CharField("登陆密码", max_length=100, null=False) - email = models.EmailField("用户邮箱", unique=True, null=False) - level = models.IntegerField("用户等级", choices=level_type, default=0) + username = models.CharField('用户名', max_length=20, unique=True, null=False) + password = models.CharField('登陆密码', max_length=100, null=False) + email = models.EmailField('用户邮箱', unique=True, null=False) + level = models.IntegerField('用户等级', choices=level_type, default=0) class UserToken(BaseTable): @@ -49,16 +49,12 @@ class Meta: db_table = "user_token" user = models.OneToOneField(to=UserInfo, on_delete=models.CASCADE, db_constraint=False) - token = models.CharField("token", max_length=50) + token = models.CharField('token', max_length=50) class MyUser(AbstractUser): - phone = models.CharField(verbose_name="手机号码", unique=True, null=True, max_length=11) - show_hosts = models.BooleanField( - verbose_name="是否显示Hosts相关的信息", - default=False, - help_text="是否显示Hosts相关的信息", - ) + phone = models.CharField(verbose_name='手机号码', unique=True, null=True, max_length=11) + show_hosts = models.BooleanField(verbose_name='是否显示Hosts相关的信息', default=False, help_text='是否显示Hosts相关的信息') class Meta(AbstractUser.Meta): pass diff --git a/fastuser/serializers.py b/fastuser/serializers.py index 0069c0cf..fb874ce2 100644 --- a/fastuser/serializers.py +++ b/fastuser/serializers.py @@ -1,8 +1,6 @@ -from django.contrib.auth import get_user_model from rest_framework import serializers - from fastuser import models - +from django.contrib.auth import get_user_model User = get_user_model() @@ -11,12 +9,20 @@ class UserInfoSerializer(serializers.Serializer): 用户信息序列化 建议实现其他方法,否则会有警告 """ + username = serializers.CharField(required=True, error_messages={ + "code": "2001", + "msg": "用户名校验失败" + }) - username = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "用户名校验失败"}) - - password = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "密码校验失败"}) + password = serializers.CharField(required=True, error_messages={ + "code": "2001", + "msg": "密码校验失败" + }) - email = serializers.CharField(required=True, error_messages={"code": "2001", "msg": "邮箱校验失败"}) + email = serializers.CharField(required=True, error_messages={ + "code": "2001", + "msg": "邮箱校验失败" + }) def create(self, validated_data): """ @@ -37,12 +43,5 @@ class UserModelSerializer(serializers.ModelSerializer): class Meta: model = User - fields = [ - "id", - "is_superuser", - "username", - "is_staff", - "is_active", - "groups", - ] + fields = ['id', 'is_superuser', 'username', 'is_staff', 'is_active', 'groups'] depth = 1 diff --git a/fastuser/urls.py b/fastuser/urls.py index ab1f7a6e..4f98b991 100644 --- a/fastuser/urls.py +++ b/fastuser/urls.py @@ -15,18 +15,13 @@ """ from django.urls import path - -from fastrunner.views import timer_task from fastuser import views +from fastrunner.views import timer_task urlpatterns = [ # 关闭注册入口,改为django admin创建用户 # path('register/', views.RegisterView.as_view()), - path("login/", views.LoginView.as_view()), - path("list/", views.UserView.as_view()), - path( - "auto_run_testsuite_pk/", - timer_task.auto_run_testsuite_pk, - name="auto_run_testsuite_pk", - ), + path('login/', views.LoginView.as_view()), + path('list/', views.UserView.as_view()), + path('auto_run_testsuite_pk/', timer_task.auto_run_testsuite_pk, name='auto_run_testsuite_pk'), ] diff --git a/fastuser/views.py b/fastuser/views.py index 200413e2..d96e2cf1 100644 --- a/fastuser/views.py +++ b/fastuser/views.py @@ -32,6 +32,7 @@ class RegisterView(APIView): """ def post(self, request): + try: username = request.data["username"] password = request.data["password"] @@ -56,7 +57,7 @@ def post(self, request): return Response(response.SYSTEM_ERROR) -def ldap_auth(username: str, password: str) -> User | None: +def ldap_auth(username: str, password: str) -> Optional[User]: ldap_user = authenticate(username=username, password=password) if ldap_user and ldap_user.backend == "django_auth_ldap.backend.LDAPBackend": logger.info(f"LDAP authentication successful for {username}") @@ -70,7 +71,7 @@ def ldap_auth(username: str, password: str) -> User | None: return None -def local_auth(username: str, password: str) -> User | None: +def local_auth(username: str, password: str) -> Optional[User]: local_user = User.objects.filter(username=username).first() if not local_user: logger.warning(f"Local user does not exist: {username}") @@ -121,8 +122,7 @@ def post(self, request): if not local_user: logger.info( - f"LDAP authentication failed or not enabled, falling back to local authentication for {username=}" - ) + f"LDAP authentication failed or not enabled, falling back to local authentication for {username=}") local_user = local_auth(username, password) if local_user: @@ -134,7 +134,6 @@ def post(self, request): else: return Response(serializer.errors) - class UserView(APIView): def get(self, request): users = User.objects.filter(is_active=1) diff --git a/httprunner/__about__.py b/httprunner/__about__.py index 59637520..211c90c2 100644 --- a/httprunner/__about__.py +++ b/httprunner/__about__.py @@ -1,9 +1,9 @@ -__title__ = "HttpRunner" -__description__ = "One-stop solution for HTTP(S) testing." -__url__ = "https://github.com/HttpRunner/HttpRunner" -__version__ = "1.5.15" -__author__ = "debugtalk" -__author_email__ = "mail@debugtalk.com" -__license__ = "MIT" -__copyright__ = "Copyright 2017 debugtalk" -__cake__ = "\u2728 \U0001f370 \u2728" +__title__ = 'HttpRunner' +__description__ = 'One-stop solution for HTTP(S) testing.' +__url__ = 'https://github.com/HttpRunner/HttpRunner' +__version__ = '1.5.15' +__author__ = 'debugtalk' +__author_email__ = 'mail@debugtalk.com' +__license__ = 'MIT' +__copyright__ = 'Copyright 2017 debugtalk' +__cake__ = u'\u2728 \U0001f370 \u2728' \ No newline at end of file diff --git a/httprunner/__init__.py b/httprunner/__init__.py index 3cc4f9c3..6547ba22 100644 --- a/httprunner/__init__.py +++ b/httprunner/__init__.py @@ -1,6 +1,10 @@ +# encoding: utf-8 + try: pass # monkey patch at beginning to avoid RecursionError when running locust. # from gevent import monkey; monkey.patch_all() except ImportError: pass + +from httprunner.api import HttpRunner diff --git a/httprunner/api.py b/httprunner/api.py index d658e7a5..ec5f9ee7 100644 --- a/httprunner/api.py +++ b/httprunner/api.py @@ -1,21 +1,15 @@ +# encoding: utf-8 import copy import logging import unittest -from httprunner import ( - exceptions, - loader, - parser, - report, - runner, - utils, - validator, -) +from httprunner import (exceptions, loader, parser, report, runner, utils, + validator) -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') -class HttpRunner: +class HttpRunner(object): def __init__(self, **kwargs): """initialize HttpRunner. @@ -77,22 +71,16 @@ def test(self): update_vars_mapping: list[dict] = test_runner.context.extractors after_vars_mapping: dict = test_runner.context.testcase_runtime_variables_mapping # 'test_0000_000' - ( - _, - case_step_index, - run_times, - ) = self._testMethodName.split("_") + _, case_step_index, run_times = self._testMethodName.split('_') case_step_name: str = self._testMethodDoc - test_runner.context.vars_trace.append( - { - "before": dict(before_vars_mapping), - "update": update_vars_mapping, - "after": dict(after_vars_mapping), - "step_name": case_step_name, - "step_index": int(case_step_index), - "run_times": int(run_times), - } - ) + test_runner.context.vars_trace.append({ + "before": dict(before_vars_mapping), + "update": update_vars_mapping, + "after": dict(after_vars_mapping), + "step_name": case_step_name, + "step_index": int(case_step_index), + "run_times": int(run_times), + }) # 赋值完之后,需要重新输出化http_client_session的meta数据,否则下次就会共享 test_runner.http_client_session.init_meta_data() @@ -120,7 +108,7 @@ def test(self): for times_index in range(int(teststep_dict.get("times", 0))): # suppose one testcase should not have more than 9999 steps, # and one step should not run more than 999 times. - test_method_name = f"test_{index:04}_{times_index + 1:03}" + test_method_name = "test_{:04}_{:03}".format(index, times_index+1) test_method = _add_teststep(test_runner, config, teststep_dict) setattr(TestSequense, test_method_name, test_method) @@ -146,7 +134,7 @@ def _run_suite(self, test_suite): for testcase in test_suite: testcase_name = testcase.config.get("name") - logger.info(f"🚀🚀🚀 Start to run testcase: {testcase_name}") + logger.info("🚀🚀🚀 Start to run testcase: {}".format(testcase_name)) result = self.unittest_runner.run(testcase) result.vars_trace = testcase.runner.context.vars_trace @@ -175,7 +163,9 @@ def _aggregate(self, tests_results): self.summary["success"] &= testcase_summary["success"] testcase_summary["name"] = testcase.config.get("name") - testcase_summary["base_url"] = testcase.config.get("request", {}).get("base_url", "") + testcase_summary["base_url"] = testcase.config.get("request", {}).get( + "base_url", "" + ) in_out = utils.get_testcase_io(testcase) utils.print_io(in_out) @@ -281,7 +271,11 @@ def gen_html_report(self, html_report_name=None, html_report_template=None): """ if not self.summary: - raise exceptions.MyBaseError("run method should be called before gen_html_report.") + raise exceptions.MyBaseError( + "run method should be called before gen_html_report." + ) self.exception_stage = "generate report" - return report.render_html_report(self.summary, html_report_name, html_report_template) + return report.render_html_report( + self.summary, html_report_name, html_report_template + ) diff --git a/httprunner/builtin/__init__.py b/httprunner/builtin/__init__.py index 1bd90505..7e8cfb92 100644 --- a/httprunner/builtin/__init__.py +++ b/httprunner/builtin/__init__.py @@ -1,14 +1,15 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py # @Time : 2021/7/26 18:08 # @Email: lihuacai168@gmail.com -from httprunner.builtin.common_util import * from httprunner.builtin.comparators import * +from httprunner.builtin.common_util import * +from httprunner.builtin.time_helper import * +from httprunner.builtin.request_helper import * from httprunner.builtin.faker_helper import * -from httprunner.builtin.login_helper import * from httprunner.builtin.rand_helper import * -from httprunner.builtin.request_helper import * -from httprunner.builtin.time_helper import * +from httprunner.builtin.login_helper import * diff --git a/httprunner/builtin/common_util.py b/httprunner/builtin/common_util.py index 218084fb..a8b91810 100644 --- a/httprunner/builtin/common_util.py +++ b/httprunner/builtin/common_util.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: common_util.py @@ -17,12 +18,15 @@ def gen_random_string(str_len): - """generate random string with specified length""" - return "".join(random.choice(string.ascii_letters + string.digits) for _ in range(str_len)) + """ generate random string with specified length + """ + return ''.join( + random.choice(string.ascii_letters + string.digits) for _ in range(str_len)) def get_timestamp(str_len=13): - """get timestamp string, length can only between 0 and 16""" + """ get timestamp string, length can only between 0 and 16 + """ if isinstance(str_len, integer_types) and 0 < str_len < 17: return builtin_str(time.time()).replace(".", "")[:str_len] @@ -30,7 +34,8 @@ def get_timestamp(str_len=13): def get_current_date(fmt="%Y-%m-%d"): - """get current date, default format is %Y-%m-%d""" + """ get current date, default format is %Y-%m-%d + """ return datetime.datetime.now().strftime(fmt) @@ -39,8 +44,10 @@ def multipart_encoder(field_name, file_path, file_type=None, file_headers=None): file_path = os.path.join(os.getcwd(), file_path) filename = os.path.basename(file_path) - with open(file_path, "rb") as f: - fields = {field_name: (filename, f.read(), file_type)} + with open(file_path, 'rb') as f: + fields = { + field_name: (filename, f.read(), file_type) + } return MultipartEncoder(fields) diff --git a/httprunner/builtin/comparators.py b/httprunner/builtin/comparators.py index ba29b7f1..f92b312a 100644 --- a/httprunner/builtin/comparators.py +++ b/httprunner/builtin/comparators.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: comparators.py @@ -104,21 +105,17 @@ def _get_expression(item, expression, expect_value, jsonpath): parsed_expression = f"{item} {expression} {expect_value}" if parsed_expression is None: - raise AssertionError("list的元素只能是dict或者string") + raise AssertionError(f"list的元素只能是dict或者string") return parsed_expression def list_any_item_contains(check_value: list, jsonpath_expression_value): assert isinstance(check_value, list) - jsonpath, expression, expect_value = jsonpath_expression_value.split(" ") + jsonpath, expression, expect_value = jsonpath_expression_value.split(' ') for item in check_value: - parsed_expression = _get_expression( - item=item, - expression=expression, - expect_value=expect_value, - jsonpath=jsonpath, - ) + parsed_expression = _get_expression(item=item, expression=expression, expect_value=expect_value, + jsonpath=jsonpath) try: if eval(parsed_expression) is True: break @@ -130,14 +127,10 @@ def list_any_item_contains(check_value: list, jsonpath_expression_value): def list_all_item_contains(check_value: list, jsonpath_expression_value): assert isinstance(check_value, list) - jsonpath, expression, expect_value = jsonpath_expression_value.split(" ") + jsonpath, expression, expect_value = jsonpath_expression_value.split(' ') for item in check_value: - parsed_expression = _get_expression( - item=item, - expression=expression, - expect_value=expect_value, - jsonpath=jsonpath, - ) + parsed_expression = _get_expression(item=item, expression=expression, expect_value=expect_value, + jsonpath=jsonpath) try: if eval(parsed_expression) is False: raise AssertionError(f"{check_value} {expression} {expect_value}") diff --git a/httprunner/builtin/faker_helper.py b/httprunner/builtin/faker_helper.py index a43f06db..521b62a0 100644 --- a/httprunner/builtin/faker_helper.py +++ b/httprunner/builtin/faker_helper.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: faker_helper.py @@ -6,7 +7,7 @@ # @Email: lihuacai168@gmail.com from faker import Faker -F = Faker(locale="zh_CN") +F = Faker(locale='zh_CN') # 假名f_name() # 假地址f_addr() @@ -16,4 +17,4 @@ f_addr = lambda: F.address() f_phone = lambda: F.phone_number() f_time = lambda: F.time() -f_date = lambda: F.date() +f_date = lambda: F.date() \ No newline at end of file diff --git a/httprunner/builtin/login_helper.py b/httprunner/builtin/login_helper.py index 769ae3e5..c240917a 100644 --- a/httprunner/builtin/login_helper.py +++ b/httprunner/builtin/login_helper.py @@ -1,32 +1,36 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: login_helper.py # @Time : 2021/8/9 17:30 # @Email: lihuacai168@gmail.com -import json import logging - +import json import pydash import requests - # from loguru import logger -uac_token_url = "http://192.168.22.19:8002/api/uac/token/" +uac_token_url = 'http://192.168.22.19:8002/api/uac/token/' -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') -def _get_token(biz, account, password, env="qa"): - data = {"biz": biz, "account": account, "password": password, "env": env} +def _get_token(biz, account, password, env='qa'): + data = { + "biz": biz, + "account": account, + "password": password, + "env": env + } headers = {"Content-Type": "application/json; charset=utf-8"} res = requests.post(url=uac_token_url, headers=headers, data=json.dumps(data)).json() return res, data -def get_userid(biz, account, password, env="qa"): +def get_userid(biz, account, password, env='qa'): res, data = _get_token(biz, account, password, env) - user_id = pydash.get(res, "user_id") + user_id = pydash.get(res, 'user_id') if user_id: logger.info(f"获取user_id成功: {user_id}") return user_id @@ -35,9 +39,9 @@ def get_userid(biz, account, password, env="qa"): raise Exception("获取user_id失败") -def get_uac_token(biz, account, password, env="qa"): +def get_uac_token(biz, account, password, env='qa'): res, data = _get_token(biz, account, password, env) - token = pydash.get(res, "token") + token = pydash.get(res, 'token') if token: logger.info(f"获取token成功: {token}") return token @@ -46,6 +50,6 @@ def get_uac_token(biz, account, password, env="qa"): raise Exception("获取token失败") -if __name__ == "__main__": - get_userid("cm", "13533975028", "397726") - get_uac_token("cm", "13533975028", "397726") +if __name__ == '__main__': + get_userid('cm', '13533975028', '397726') + get_uac_token('cm', '13533975028', '397726') diff --git a/httprunner/builtin/rand_helper.py b/httprunner/builtin/rand_helper.py index 12b2e030..9bb271b3 100644 --- a/httprunner/builtin/rand_helper.py +++ b/httprunner/builtin/rand_helper.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: rand_helper.py @@ -10,27 +11,32 @@ def rand_int(begin: int = 0, end: int = 10000): - """生成0-10000的随机数""" + """生成0-10000的随机数 + """ return random.randint(begin, end) def rand_int4(): - """4位随机数""" + """4位随机数 + """ return rand_int(1000, 9999) def rand_int5(): - """5位随机数""" + """5位随机数 + """ return rand_int(10000, 99999) def rand_int6(): - """6位随机数""" + """6位随机数 + """ return rand_int(100000, 999999) def rand_str(n: int = 5): - """获取大小写字母+数字的随机字符串,默认5位""" + """获取大小写字母+数字的随机字符串,默认5位 + """ seq = string.ascii_letters + string.digits return "".join(random.choices(seq, k=n)) diff --git a/httprunner/builtin/request_helper.py b/httprunner/builtin/request_helper.py index 23869e34..7b24af8f 100644 --- a/httprunner/builtin/request_helper.py +++ b/httprunner/builtin/request_helper.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: request_helper.py @@ -12,7 +13,7 @@ # from loguru import logger -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def _load_json(in_data): diff --git a/httprunner/builtin/time_helper.py b/httprunner/builtin/time_helper.py index ed52ebf4..3ca96a90 100644 --- a/httprunner/builtin/time_helper.py +++ b/httprunner/builtin/time_helper.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: time_helper.py @@ -35,8 +36,8 @@ def get_day(days: int = 0, **kwargs): """ d = datetime.timedelta(days) n = datetime.datetime.now() - sep: str = kwargs.get("sep", "") - fmt = sep.join(["%Y", "%m", "%d"]) + sep: str = kwargs.get('sep', '') + fmt = sep.join(['%Y', '%m', '%d']) if kwargs: h = kwargs.get("h", "00") m = kwargs.get("m", "00") @@ -45,7 +46,7 @@ def get_day(days: int = 0, **kwargs): return (n + d).strftime(fmt) -def get_day_fmt(fmt_type="sec", **kwargs): +def get_day_fmt(fmt_type='sec', **kwargs): """获取日期 fmt_type : str @@ -66,8 +67,8 @@ def get_day_fmt(fmt_type="sec", **kwargs): >>> get_day_fmt('day', days=-1) 2021-08-06 """ - fmt = "%Y-%m-%d %H:%m:%S" - if fmt_type == "day": + fmt = '%Y-%m-%d %H:%m:%S' + if fmt_type == 'day': fmt = "%Y-%m-%d" day = datetime.timedelta(**kwargs) @@ -82,10 +83,10 @@ def get_day_h(days: int = 0, **kwargs): """ d = datetime.timedelta(days) n = datetime.datetime.now() - time_str = "%Y%m%d" + time_str = f"%Y%m%d" if kwargs: h = str(kwargs.get("h")) - h = h.rjust(2, "0") + h = h.rjust(2, '0') time_str = f"{time_str}{h}" return (n + d).strftime(time_str) @@ -133,7 +134,7 @@ def get_hour_ts(interval: int = 0) -> str: def get_hour() -> str: # 获取当前的小时,不足两位补0 - return str(datetime.datetime.now().hour).rjust(2, "0") + return str(datetime.datetime.now().hour).rjust(2, '0') def get_hour_ts_int(interval: int = 0) -> int: diff --git a/httprunner/cli.py b/httprunner/cli.py index a1126214..de085823 100644 --- a/httprunner/cli.py +++ b/httprunner/cli.py @@ -1,63 +1,61 @@ +# encoding: utf-8 + import argparse -import logging import multiprocessing +import logging import sys +import unittest # from httprunner import logger from httprunner.__about__ import __description__, __version__ from httprunner.api import HttpRunner from httprunner.compat import is_py2 -from httprunner.utils import ( - create_scaffold, - get_python2_retire_msg, - prettify_json_file, - validate_json_file, -) +from httprunner.utils import (create_scaffold, get_python2_retire_msg, + prettify_json_file, validate_json_file) -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def main_hrun(): - """API test: parse command line options and run commands.""" + """ API test: parse command line options and run commands. + """ parser = argparse.ArgumentParser(description=__description__) parser.add_argument( - "-V", - "--version", - dest="version", - action="store_true", - help="show version", - ) - parser.add_argument("testcase_paths", nargs="*", help="testcase file path") + '-V', '--version', dest='version', action='store_true', + help="show version") + parser.add_argument( + 'testcase_paths', nargs='*', + help="testcase file path") + parser.add_argument( + '--no-html-report', action='store_true', default=False, + help="do not generate html report.") + parser.add_argument( + '--html-report-name', + help="specify html report name, only effective when generating html report.") parser.add_argument( - "--no-html-report", - action="store_true", - default=False, - help="do not generate html report.", - ) + '--html-report-template', + help="specify html report template path.") parser.add_argument( - "--html-report-name", - help="specify html report name, only effective when generating html report.", - ) - parser.add_argument("--html-report-template", help="specify html report template path.") + '--log-level', default='INFO', + help="Specify logging level, default is INFO.") parser.add_argument( - "--log-level", - default="INFO", - help="Specify logging level, default is INFO.", - ) - parser.add_argument("--log-file", help="Write logs to specified file path.") + '--log-file', + help="Write logs to specified file path.") parser.add_argument( - "--dot-env-path", - help="Specify .env file path, which is useful for keeping sensitive data.", - ) + '--dot-env-path', + help="Specify .env file path, which is useful for keeping sensitive data.") parser.add_argument( - "--failfast", - action="store_true", - default=False, - help="Stop the test run on the first error or failure.", - ) - parser.add_argument("--startproject", help="Specify new project name.") - parser.add_argument("--validate", nargs="*", help="Validate JSON testcase format.") - parser.add_argument("--prettify", nargs="*", help="Prettify JSON testcase format.") + '--failfast', action='store_true', default=False, + help="Stop the test run on the first error or failure.") + parser.add_argument( + '--startproject', + help="Specify new project name.") + parser.add_argument( + '--validate', nargs='*', + help="Validate JSON testcase format.") + parser.add_argument( + '--prettify', nargs='*', + help="Prettify JSON testcase format.") args = parser.parse_args() # logger.setup_logger(args.log_level, args.log_file) @@ -66,7 +64,7 @@ def main_hrun(): logger.warning(get_python2_retire_msg()) if args.version: - logger.info(f"{__version__}", "GREEN") + logger.info("{}".format(__version__), "GREEN") exit(0) @@ -83,24 +81,29 @@ def main_hrun(): exit(0) try: - runner = HttpRunner(failfast=args.failfast) - runner.run(args.testcase_paths, dot_env_path=args.dot_env_path) + runner = HttpRunner( + failfast=args.failfast + ) + runner.run( + args.testcase_paths, + dot_env_path=args.dot_env_path + ) except Exception: - logger.error(f"!!!!!!!!!! exception stage: {runner.exception_stage} !!!!!!!!!!") + logger.error("!!!!!!!!!! exception stage: {} !!!!!!!!!!".format(runner.exception_stage)) raise if not args.no_html_report: runner.gen_html_report( html_report_name=args.html_report_name, - html_report_template=args.html_report_template, + html_report_template=args.html_report_template ) summary = runner.summary return 0 if summary["success"] else 1 - def main_locust(): - """Performance test with locust: parse command line options and run commands.""" + """ Performance test with locust: parse command line options and run commands. + """ try: from httprunner import locusts except ImportError: @@ -109,7 +112,7 @@ def main_locust(): print(msg) exit(1) - sys.argv[0] = "locust" + sys.argv[0] = 'locust' if len(sys.argv) == 1: sys.argv.extend(["-h"]) @@ -119,9 +122,9 @@ def main_locust(): # set logging level if "-L" in sys.argv: - loglevel_index = sys.argv.index("-L") + 1 + loglevel_index = sys.argv.index('-L') + 1 elif "--loglevel" in sys.argv: - loglevel_index = sys.argv.index("--loglevel") + 1 + loglevel_index = sys.argv.index('--loglevel') + 1 else: loglevel_index = None @@ -136,9 +139,9 @@ def main_locust(): # get testcase file path try: if "-f" in sys.argv: - testcase_index = sys.argv.index("-f") + 1 + testcase_index = sys.argv.index('-f') + 1 elif "--locustfile" in sys.argv: - testcase_index = sys.argv.index("--locustfile") + 1 + testcase_index = sys.argv.index('--locustfile') + 1 else: testcase_index = None @@ -157,7 +160,7 @@ def main_locust(): logger.error("conflict parameter args: --processes & --no-web. \nexit.") sys.exit(1) - processes_index = sys.argv.index("--processes") + processes_index = sys.argv.index('--processes') processes_count_index = processes_index + 1 diff --git a/httprunner/client.py b/httprunner/client.py index 7935aa3f..95370bf9 100644 --- a/httprunner/client.py +++ b/httprunner/client.py @@ -1,31 +1,31 @@ -import logging +# encoding: utf-8 + import re + +import curlify import time +import logging import requests import urllib3 -from requests import Request, Response -from requests.exceptions import ( - InvalidSchema, - InvalidURL, - MissingSchema, - RequestException, -) - # from httprunner import logger from httprunner.exceptions import ParamsError +from requests import Request, Response +from requests.exceptions import (InvalidSchema, InvalidURL, MissingSchema, + RequestException) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) absolute_http_url_regexp = re.compile(r"^https?://", re.I) -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') class ApiResponse(Response): + def raise_for_status(self): - if hasattr(self, "error") and self.error: + if hasattr(self, 'error') and self.error: raise self.error Response.raise_for_status(self) @@ -43,14 +43,13 @@ class HttpSession(requests.Session): part of the URL will be prepended with the HttpSession.base_url which is normally inherited from a HttpRunner class' host property. """ - def __init__(self, base_url=None, *args, **kwargs): super(HttpSession, self).__init__(*args, **kwargs) self.base_url = base_url if base_url else "" self.init_meta_data() def _build_url(self, path): - """prepend url with hostname unless it's already an absolute URL""" + """ prepend url with hostname unless it's already an absolute URL """ if absolute_http_url_regexp.match(path): return path elif self.base_url: @@ -59,13 +58,14 @@ def _build_url(self, path): raise ParamsError("base url missed!") def init_meta_data(self): - """initialize meta_data, it will store detail data of request and response""" + """ initialize meta_data, it will store detail data of request and response + """ self.meta_data = { "request": { "url": "N/A", "method": "N/A", "headers": {}, - "start_timestamp": None, + "start_timestamp": None }, "response": { "status_code": "N/A", @@ -75,9 +75,9 @@ def init_meta_data(self): "elapsed_ms": "N/A", "encoding": None, "content": None, - "content_type": "", + "content_type": "" }, - "validators": [], + "validators": [], "logs": [], "extractors": [], } @@ -121,11 +121,10 @@ def request(self, method, url, name=None, **kwargs): :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. """ - def log_print(request_response): msg = "\n================== {} details ==================\n".format(request_response) for key, value in self.meta_data[request_response].items(): - msg += f"{key:<16} : {repr(value)}\n" + msg += "{:<16} : {}\n".format(key, repr(value)) logger.debug(msg) # record original request info @@ -141,10 +140,8 @@ def log_print(request_response): response = self._send_request_safe_mode(method, url, **kwargs) # record the consumed time - self.meta_data["response"]["response_time_ms"] = round( - (time.time() - self.meta_data["request"]["start_timestamp"]) * 1000, - 2, - ) + self.meta_data["response"]["response_time_ms"] = \ + round((time.time() - self.meta_data["request"]["start_timestamp"]) * 1000, 2) self.meta_data["response"]["elapsed_ms"] = response.elapsed.microseconds / 1000.0 # record actual request info @@ -175,9 +172,7 @@ def log_print(request_response): # get the length of the content, but if the argument stream is set to True, we take # the size from the content-length header, in order to not trigger fetching of the body if kwargs.get("stream", False): - self.meta_data["response"]["content_size"] = int( - self.meta_data["response"]["headers"].get("content-length") or 0 - ) + self.meta_data["response"]["content_size"] = int(self.meta_data["response"]["headers"].get("content-length") or 0) else: self.meta_data["response"]["content_size"] = len(response.content or "") @@ -187,13 +182,13 @@ def log_print(request_response): try: response.raise_for_status() except RequestException as e: - logger.error(f"{str(e)}") + logger.error(u"{exception}".format(exception=str(e))) else: logger.info( """status_code: {}, response_time(ms): {} ms, response_length: {} bytes""".format( self.meta_data["response"]["status_code"], self.meta_data["response"]["response_time_ms"], - self.meta_data["response"]["content_size"], + self.meta_data["response"]["content_size"] ) ) @@ -206,8 +201,8 @@ def _send_request_safe_mode(self, method, url, **kwargs): """ try: msg = "processed request:\n" - msg += f"> {method} {url}\n" - msg += f"> kwargs: {kwargs}" + msg += "> {method} {url}\n".format(method=method, url=url) + msg += "> kwargs: {kwargs}".format(kwargs=kwargs) logger.debug(msg) return requests.Session.request(self, method, url, **kwargs) except (MissingSchema, InvalidSchema, InvalidURL): diff --git a/httprunner/compat.py b/httprunner/compat.py index 3e318fc5..72716b22 100644 --- a/httprunner/compat.py +++ b/httprunner/compat.py @@ -1,3 +1,5 @@ +# encoding: utf-8 + """ httprunner.compat ~~~~~~~~~~~~~~~~~ @@ -5,6 +7,7 @@ This module handles import compatibility issues between Python 2 and Python 3. """ +from collections import OrderedDict try: import simplejson as json @@ -21,10 +24,10 @@ _ver = sys.version_info #: Python 2.x? -is_py2 = _ver[0] == 2 +is_py2 = (_ver[0] == 2) #: Python 3.x? -is_py3 = _ver[0] == 3 +is_py3 = (_ver[0] == 3) # --------- diff --git a/httprunner/context.py b/httprunner/context.py index 890b184f..56b92f05 100644 --- a/httprunner/context.py +++ b/httprunner/context.py @@ -1,19 +1,21 @@ +# encoding: utf-8 + import copy import logging from httprunner import exceptions, parser, utils from httprunner.compat import OrderedDict -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') -class Context: - """Manages context functions and variables. - context has two levels, testcase and teststep. +class Context(object): + """ Manages context functions and variables. + context has two levels, testcase and teststep. """ - def __init__(self, variables=None, functions=None): - """init Context with testcase variables and functions.""" + """ init Context with testcase variables and functions. + """ # testcase level context # TESTCASE_SHARED_FUNCTIONS_MAPPING are unchangeable. # TESTCASE_SHARED_VARIABLES_MAPPING may change by Hrun.set_config @@ -37,7 +39,7 @@ def __init__(self, variables=None, functions=None): self.vars_trace: list[dict] = [] def init_context_variables(self, level="testcase"): - """initialize testcase/teststep context + """ initialize testcase/teststep context Args: level (enum): "testcase" or "teststep" @@ -53,7 +55,7 @@ def init_context_variables(self, level="testcase"): self.teststep_variables_mapping = copy.deepcopy(self.testcase_runtime_variables_mapping) def update_context_variables(self, variables, level): - """update context variables, with level specified. + """ update context variables, with level specified. Args: variables (list/OrderedDict): testcase config block or teststep block @@ -84,17 +86,18 @@ def update_context_variables(self, variables, level): self.update_teststep_variables_mapping(variable_name, variable_eval_value) def eval_content(self, content): - """evaluate content recursively, take effect on each variable and function in content. - content may be in any data structure, include dict, list, tuple, number, string, etc. + """ evaluate content recursively, take effect on each variable and function in content. + content may be in any data structure, include dict, list, tuple, number, string, etc. """ return parser.parse_data( content, self.teststep_variables_mapping, - self.TESTCASE_SHARED_FUNCTIONS_MAPPING, + self.TESTCASE_SHARED_FUNCTIONS_MAPPING ) + def update_testcase_runtime_variables_mapping(self, variables): - """update testcase_runtime_variables_mapping with extracted vairables in teststep. + """ update testcase_runtime_variables_mapping with extracted vairables in teststep. Args: variables (OrderDict): extracted variables in teststep @@ -105,11 +108,12 @@ def update_testcase_runtime_variables_mapping(self, variables): self.update_teststep_variables_mapping(variable_name, variable_value) def update_teststep_variables_mapping(self, variable_name, variable_value): - """bind and update testcase variables mapping""" + """ bind and update testcase variables mapping + """ self.teststep_variables_mapping[variable_name] = variable_value def get_parsed_request(self, request_dict, level="teststep"): - """get parsed request with variables and functions. + """ get parsed request with variables and functions. Args: request_dict (dict): request config mapping @@ -129,12 +133,12 @@ def get_parsed_request(self, request_dict, level="teststep"): return self.eval_content( utils.deep_update_dict( copy.deepcopy(self.TESTCASE_SHARED_REQUEST_MAPPING), - request_dict, + request_dict ) ) def __eval_check_item(self, validator, resp_obj): - """evaluate check item in validator. + """ evaluate check item in validator. Args: validator (dict): validator @@ -160,11 +164,9 @@ def __eval_check_item(self, validator, resp_obj): # 4, string joined by delimiter. e.g. "status_code", "headers.content-type" # 5, regex string, e.g. "LB[\d]*(.*)RB[\d]*" - if ( - isinstance(check_item, (dict, list)) - or parser.extract_variables(check_item) - or parser.extract_functions(check_item) - ): + if isinstance(check_item, (dict, list)) \ + or parser.extract_variables(check_item) \ + or parser.extract_functions(check_item): # format 1/2/3 check_value = self.eval_content(check_item) @@ -187,7 +189,7 @@ def __eval_check_item(self, validator, resp_obj): return validator def _do_validation(self, validator_dict): - """validate with functions + """ validate with functions Args: validator_dict (dict): validator dict @@ -207,17 +209,15 @@ def _do_validation(self, validator_dict): check_value = validator_dict["check_value"] expect_value = validator_dict["expect"] - if (check_value is None or expect_value is None) and comparator not in [ - "is", - "eq", - "equals", - "not_equals", - "==", - ]: + if (check_value is None or expect_value is None) \ + and comparator not in ["is", "eq", "equals", "not_equals", "=="]: raise exceptions.ParamsError("Null value can only be compared with comparator: eq/equals/==") validate_msg = "validate expression: {} {} {}({})".format( - check_item, comparator, expect_value, type(expect_value).__name__ + check_item, + comparator, + expect_value, + type(expect_value).__name__ ) try: @@ -232,14 +232,15 @@ def _do_validation(self, validator_dict): type(check_value).__name__, comparator, expect_value, - type(expect_value).__name__, + type(expect_value).__name__ ) logger.error(validate_msg) validator_dict["check_result"] = "fail" raise exceptions.ValidationFailure(validate_msg) def validate(self, validators, resp_obj) -> (bool, list[dict]): - """make validations""" + """ make validations + """ logger.info("start to validate.") is_validate_passed = True evaluated_validators = [] @@ -251,8 +252,11 @@ def validate(self, validators, resp_obj) -> (bool, list[dict]): for validator in validators: # evaluate validators with context variable mapping. - evaluated_validator = self.__eval_check_item(parser.parse_validator(validator), resp_obj) - evaluated_validator["validate_msg"] = "ok" + evaluated_validator = self.__eval_check_item( + parser.parse_validator(validator), + resp_obj + ) + evaluated_validator['validate_msg'] = 'ok' try: self._do_validation(evaluated_validator) @@ -262,7 +266,7 @@ def validate(self, validators, resp_obj) -> (bool, list[dict]): logger.info("❌❌❌ validate failed: %s", str(ex)) finally: if fail_msg: - evaluated_validator["validate_msg"] = fail_msg + evaluated_validator['validate_msg'] = fail_msg fail_msg = "" evaluated_validators.append(evaluated_validator) if not is_validate_passed: diff --git a/httprunner/exceptions.py b/httprunner/exceptions.py index b93e4146..ce5c7e46 100644 --- a/httprunner/exceptions.py +++ b/httprunner/exceptions.py @@ -1,4 +1,6 @@ -from httprunner.compat import FileNotFoundError +# encoding: utf-8 + +from httprunner.compat import JSONDecodeError, FileNotFoundError """ failure type exceptions these exceptions will mark test as failure diff --git a/httprunner/loader.py b/httprunner/loader.py index 6bd48990..8e90531f 100644 --- a/httprunner/loader.py +++ b/httprunner/loader.py @@ -3,53 +3,54 @@ import importlib import io import json -import logging import os import sys +import logging import yaml - from httprunner import builtin, exceptions, parser, utils, validator from httprunner.compat import OrderedDict -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') ############################################################################### ## file loader ############################################################################### - def _check_format(file_path, content): - """check testcase format if valid""" + """ check testcase format if valid + """ # TODO: replace with JSON schema validation if not content: # testcase file content is empty - err_msg = f"Testcase file content is empty: {file_path}" + err_msg = u"Testcase file content is empty: {}".format(file_path) logger.error(err_msg) raise exceptions.FileFormatError(err_msg) elif not isinstance(content, (list, dict)): # testcase file content does not match testcase format - err_msg = f"Testcase file content format invalid: {file_path}" + err_msg = u"Testcase file content format invalid: {}".format(file_path) logger.error(err_msg) raise exceptions.FileFormatError(err_msg) def load_yaml_file(yaml_file): - """load yaml file and check file content format""" - with open(yaml_file, encoding="utf-8") as stream: + """ load yaml file and check file content format + """ + with io.open(yaml_file, 'r', encoding='utf-8') as stream: yaml_content = yaml.load(stream) _check_format(yaml_file, yaml_content) return yaml_content def load_json_file(json_file): - """load json file and check file content format""" - with open(json_file, encoding="utf-8") as data_file: + """ load json file and check file content format + """ + with io.open(json_file, encoding='utf-8') as data_file: try: json_content = json.load(data_file) except exceptions.JSONDecodeError: - err_msg = f"JSONDecodeError: JSON file format error: {json_file}" + err_msg = u"JSONDecodeError: JSON file format error: {}".format(json_file) logger.error(err_msg) raise exceptions.FileFormatError(err_msg) @@ -58,7 +59,7 @@ def load_json_file(json_file): def load_csv_file(csv_file): - """load csv file and check file content format + """ load csv file and check file content format @param csv_file: csv file path e.g. csv file content: @@ -77,7 +78,7 @@ def load_csv_file(csv_file): """ csv_content_list = [] - with open(csv_file, encoding="utf-8") as csvfile: + with io.open(csv_file, encoding='utf-8') as csvfile: reader = csv.DictReader(csvfile) for row in reader: csv_content_list.append(row) @@ -87,24 +88,24 @@ def load_csv_file(csv_file): def load_file(file_path): if not os.path.isfile(file_path): - raise exceptions.FileNotFound(f"{file_path} does not exist.") + raise exceptions.FileNotFound("{} does not exist.".format(file_path)) file_suffix = os.path.splitext(file_path)[1].lower() - if file_suffix == ".json": + if file_suffix == '.json': return load_json_file(file_path) - elif file_suffix in [".yaml", ".yml"]: + elif file_suffix in ['.yaml', '.yml']: return load_yaml_file(file_path) elif file_suffix == ".csv": return load_csv_file(file_path) else: # '' or other suffix - err_msg = f"Unsupported file format: {file_path}" + err_msg = u"Unsupported file format: {}".format(file_path) logger.warning(err_msg) return [] def load_folder_files(folder_path, recursive=True): - """load folder path, return all files endswith yml/yaml/json in list. + """ load folder path, return all files endswith yml/yaml/json in list. Args: folder_path (str): specified folder path to load @@ -129,7 +130,7 @@ def load_folder_files(folder_path, recursive=True): filenames_list = [] for filename in filenames: - if not filename.endswith((".yml", ".yaml", ".json")): + if not filename.endswith(('.yml', '.yaml', '.json')): continue filenames_list.append(filename) @@ -145,7 +146,7 @@ def load_folder_files(folder_path, recursive=True): def load_dot_env_file(dot_env_path): - """load .env file. + """ load .env file. Args: dot_env_path (str): .env file path @@ -166,9 +167,9 @@ def load_dot_env_file(dot_env_path): if not os.path.isfile(dot_env_path): raise exceptions.FileNotFound(".env file path is not exist.") - logger.info(f"Loading environment variables from {dot_env_path}") + logger.info("Loading environment variables from {}".format(dot_env_path)) env_variables_mapping = {} - with open(dot_env_path, encoding="utf-8") as fp: + with io.open(dot_env_path, 'r', encoding='utf-8') as fp: for line in fp: # maxsplit=1 if "=" in line: @@ -185,7 +186,7 @@ def load_dot_env_file(dot_env_path): def locate_file(start_path, file_name): - """locate filename and return file path. + """ locate filename and return file path. searching will be recursive upward until current working directory. Args: @@ -203,18 +204,15 @@ def locate_file(start_path, file_name): elif os.path.isdir(start_path): start_dir_path = start_path else: - raise exceptions.FileNotFound(f"invalid path: {start_path}") + raise exceptions.FileNotFound("invalid path: {}".format(start_path)) file_path = os.path.join(start_dir_path, file_name) if os.path.isfile(file_path): return file_path # current working directory - if os.path.abspath(start_dir_path) in [ - os.getcwd(), - os.path.abspath(os.sep), - ]: - raise exceptions.FileNotFound(f"{file_name} not found in {start_path}") + if os.path.abspath(start_dir_path) in [os.getcwd(), os.path.abspath(os.sep)]: + raise exceptions.FileNotFound("{} not found in {}".format(file_name, start_path)) # locate recursive upward return locate_file(os.path.dirname(start_dir_path), file_name) @@ -224,9 +222,8 @@ def locate_file(start_path, file_name): ## debugtalk.py module loader ############################################################################### - def load_python_module(module): - """load python module. + """ load python module. Args: module: python module @@ -240,7 +237,10 @@ def load_python_module(module): } """ - debugtalk_module = {"variables": {}, "functions": {}} + debugtalk_module = { + "variables": {}, + "functions": {} + } for name, item in vars(module).items(): if validator.is_function((name, item)): @@ -256,13 +256,14 @@ def load_python_module(module): def load_builtin_module(): - """load built_in module""" + """ load built_in module + """ built_in_module = load_python_module(builtin) return built_in_module def load_debugtalk_module(): - """load project debugtalk.py module + """ load project debugtalk.py module debugtalk.py should be located in project working directory. Returns: @@ -280,7 +281,7 @@ def load_debugtalk_module(): def get_module_item(module_mapping, item_type, item_name): - """get expected function or variable from module mapping. + """ get expected function or variable from module mapping. Args: module_mapping(dict): module mapping with variables and functions. @@ -304,8 +305,8 @@ def get_module_item(module_mapping, item_type, item_name): try: return module_mapping[item_type][item_name] except KeyError: - err_msg = f"{item_name} not found in debugtalk.py module!\n" - err_msg += f"module mapping: {module_mapping}" + err_msg = "{} not found in debugtalk.py module!\n".format(item_name) + err_msg += "module mapping: {}".format(module_mapping) if item_type == "functions": raise exceptions.FunctionNotFound(err_msg) else: @@ -316,9 +317,8 @@ def get_module_item(module_mapping, item_type, item_name): ## testcase loader ############################################################################### - def _load_teststeps(test_block, project_mapping): - """load teststeps with api/testcase references + """ load teststeps with api/testcase references Args: test_block (dict): test block content, maybe in 3 formats. @@ -345,7 +345,6 @@ def _load_teststeps(test_block, project_mapping): list: loaded teststeps list """ - def extend_api_definition(block): ref_call = block["api"] def_block = _get_block_by_name(ref_call, "def-api", project_mapping) @@ -359,7 +358,7 @@ def extend_api_definition(block): teststeps.append(test_block) # reference testcase - elif "suite" in test_block: # TODO: replace suite with testcase + elif "suite" in test_block: # TODO: replace suite with testcase ref_call = test_block["suite"] block = _get_block_by_name(ref_call, "def-testcase", project_mapping) # TODO: bugfix lost block config variables @@ -376,7 +375,7 @@ def extend_api_definition(block): def _load_testcase(raw_testcase, project_mapping): - """load testcase/testsuite with api/testcase references + """ load testcase/testsuite with api/testcase references Args: raw_testcase (list): raw testcase content loaded from JSON/YAML file: @@ -407,16 +406,19 @@ def _load_testcase(raw_testcase, project_mapping): } """ - loaded_testcase = {"config": {}, "teststeps": []} + loaded_testcase = { + "config": {}, + "teststeps": [] + } for item in raw_testcase: # TODO: add json schema validation if not isinstance(item, dict) or len(item) != 1: - raise exceptions.FileFormatError(f"Testcase format error: {item}") + raise exceptions.FileFormatError("Testcase format error: {}".format(item)) key, test_block = item.popitem() if not isinstance(test_block, dict): - raise exceptions.FileFormatError(f"Testcase format error: {item}") + raise exceptions.FileFormatError("Testcase format error: {}".format(item)) if key == "config": loaded_testcase["config"].update(test_block) @@ -425,13 +427,15 @@ def _load_testcase(raw_testcase, project_mapping): loaded_testcase["teststeps"].extend(_load_teststeps(test_block, project_mapping)) else: - logger.warning(f"unexpected block key: {key}. block key should only be 'config' or 'test'.") + logger.warning( + "unexpected block key: {}. block key should only be 'config' or 'test'.".format(key) + ) return loaded_testcase def _get_block_by_name(ref_call, ref_type, project_mapping): - """get test content by reference name. + """ get test content by reference name. Args: ref_call (str): call function. @@ -454,8 +458,8 @@ def _get_block_by_name(ref_call, ref_type, project_mapping): if len(call_args) != len(def_args): err_msg = "{}: call args number is not equal to defined args number!\n".format(func_name) - err_msg += f"defined args: {def_args}\n" - err_msg += f"reference args: {call_args}" + err_msg += "defined args: {}\n".format(def_args) + err_msg += "reference args: {}".format(call_args) logger.error(err_msg) raise exceptions.ParamsError(err_msg) @@ -473,7 +477,7 @@ def _get_block_by_name(ref_call, ref_type, project_mapping): def _get_test_definition(name, ref_type, project_mapping): - """get expected api or testcase. + """ get expected api or testcase. Args: name (str): api or testcase name @@ -491,7 +495,7 @@ def _get_test_definition(name, ref_type, project_mapping): block = project_mapping.get(ref_type, {}).get(name) if not block: - err_msg = f"{name} not found!" + err_msg = "{} not found!".format(name) if ref_type == "def-api": raise exceptions.ApiNotFound(err_msg) else: @@ -502,7 +506,7 @@ def _get_test_definition(name, ref_type, project_mapping): def _extend_block(ref_block, def_block): - """extend ref_block with def_block. + """ extend ref_block with def_block. Args: def_block (dict): api definition dict. @@ -535,16 +539,26 @@ def _extend_block(ref_block, def_block): def_validators = def_block.get("validate") or def_block.get("validators", []) ref_validators = ref_block.get("validate") or ref_block.get("validators", []) - def_extrators = def_block.get("extract") or def_block.get("extractors") or def_block.get("extract_binds", []) - ref_extractors = ref_block.get("extract") or ref_block.get("extractors") or ref_block.get("extract_binds", []) + def_extrators = def_block.get("extract") \ + or def_block.get("extractors") \ + or def_block.get("extract_binds", []) + ref_extractors = ref_block.get("extract") \ + or ref_block.get("extractors") \ + or ref_block.get("extract_binds", []) ref_block.update(def_block) - ref_block["validate"] = _merge_validator(def_validators, ref_validators) - ref_block["extract"] = _merge_extractor(def_extrators, ref_extractors) + ref_block["validate"] = _merge_validator( + def_validators, + ref_validators + ) + ref_block["extract"] = _merge_extractor( + def_extrators, + ref_extractors + ) def _convert_validators_to_mapping(validators): - """convert validators list to mapping. + """ convert validators list to mapping. Args: validators (list): validators in list @@ -581,7 +595,7 @@ def _convert_validators_to_mapping(validators): def _merge_validator(def_validators, ref_validators): - """merge def_validators with ref_validators. + """ merge def_validators with ref_validators. Args: def_validators (list): @@ -616,7 +630,7 @@ def _merge_validator(def_validators, ref_validators): def _merge_extractor(def_extrators, ref_extractors): - """merge def_extrators with ref_extractors + """ merge def_extrators with ref_extractors Args: def_extrators (list): [{"var1": "val1"}, {"var2": "val2"}] @@ -646,7 +660,7 @@ def _merge_extractor(def_extrators, ref_extractors): extractor_dict = OrderedDict() for api_extrator in def_extrators: if len(api_extrator) != 1: - logger.warning(f"incorrect extractor: {api_extrator}") + logger.warning("incorrect extractor: {}".format(api_extrator)) continue var_name = list(api_extrator.keys())[0] @@ -654,7 +668,7 @@ def _merge_extractor(def_extrators, ref_extractors): for test_extrator in ref_extractors: if len(test_extrator) != 1: - logger.warning(f"incorrect extractor: {test_extrator}") + logger.warning("incorrect extractor: {}".format(test_extrator)) continue var_name = list(test_extrator.keys())[0] @@ -668,7 +682,7 @@ def _merge_extractor(def_extrators, ref_extractors): def load_folder_content(folder_path): - """load api/testcases/testsuites definitions from folder. + """ load api/testcases/testsuites definitions from folder. Args: folder_path (str): api/testcases/testsuites files folder. @@ -693,7 +707,7 @@ def load_folder_content(folder_path): def load_api_folder(api_folder_path): - """load api definitions from api folder. + """ load api definitions from api folder. Args: api_folder_path (str): api files folder. @@ -745,7 +759,7 @@ def load_api_folder(api_folder_path): func_name = function_meta["func_name"] if func_name in api_definition_mapping: - logger.warning(f"API definition duplicated: {func_name}") + logger.warning("API definition duplicated: {}".format(func_name)) api_dict["function_meta"] = function_meta api_definition_mapping[func_name] = api_dict @@ -754,7 +768,7 @@ def load_api_folder(api_folder_path): def load_test_folder(test_folder_path): - """load testcases definitions from folder. + """ load testcases definitions from folder. Args: test_folder_path (str): testcases files folder. @@ -800,7 +814,10 @@ def load_test_folder(test_folder_path): for test_file_path, items in test_items_mapping.items(): # TODO: add JSON schema validation - testcase = {"config": {}, "teststeps": []} + testcase = { + "config": {}, + "teststeps": [] + } for item in items: key, block = item.popitem() @@ -816,7 +833,7 @@ def load_test_folder(test_folder_path): func_name = function_meta["func_name"] if func_name in test_definition_mapping: - logger.warning(f"API definition duplicated: {func_name}") + logger.warning("API definition duplicated: {}".format(func_name)) testcase["function_meta"] = function_meta test_definition_mapping[func_name] = testcase @@ -828,7 +845,7 @@ def load_test_folder(test_folder_path): def locate_debugtalk_py(start_path): - """locate debugtalk.py file. + """ locate debugtalk.py file. Args: start_path (str): start locating path, maybe testcase file path or directory path @@ -842,7 +859,7 @@ def locate_debugtalk_py(start_path): def load_project_tests(test_path, dot_env_path=None): - """load api, testcases, .env, builtin module and debugtalk.py. + """ load api, testcases, .env, builtin module and debugtalk.py. api/testcases folder is relative to project_working_directory Args: @@ -878,7 +895,10 @@ def load_project_tests(test_path, dot_env_path=None): if debugtalk_path: project_mapping["debugtalk"] = load_debugtalk_module() else: - project_mapping["debugtalk"] = {"variables": {}, "functions": {}} + project_mapping["debugtalk"] = { + "variables": {}, + "functions": {} + } project_mapping["def-api"] = load_api_folder(os.path.join(project_working_directory, "api")) # TODO: replace suite with testcases @@ -888,7 +908,7 @@ def load_project_tests(test_path, dot_env_path=None): def load_tests(path, dot_env_path=None): - """load testcases from file path, extend and merge with api/testcase definitions. + """ load testcases from file path, extend and merge with api/testcase definitions. Args: path (str/list): testcase file/foler path. @@ -946,7 +966,7 @@ def load_tests(path, dot_env_path=None): return testcases_list if not os.path.exists(path): - err_msg = f"path not exist: {path}" + err_msg = "path not exist: {}".format(path) logger.error(err_msg) raise exceptions.FileNotFound(err_msg) @@ -972,7 +992,7 @@ def load_tests(path, dot_env_path=None): def load_locust_tests(path, dot_env_path=None): - """load locust testcases + """ load locust testcases Args: path (str): testcase/testsuite file path. @@ -997,7 +1017,9 @@ def load_locust_tests(path, dot_env_path=None): raw_testcase = load_file(path) project_mapping = load_project_tests(path, dot_env_path) - config = {"refs": project_mapping} + config = { + "refs": project_mapping + } tests = [] for item in raw_testcase: key, test_block = item.popitem() @@ -1010,4 +1032,7 @@ def load_locust_tests(path, dot_env_path=None): for _ in range(weight): tests.append(teststeps) - return {"config": config, "tests": tests} + return { + "config": config, + "tests": tests + } diff --git a/httprunner/locustfile.py b/httprunner/locustfile.py index 7e52d2ff..b9482354 100644 --- a/httprunner/locustfile.py +++ b/httprunner/locustfile.py @@ -1,22 +1,23 @@ +# coding: utf-8 import json import logging import os import random import sys -from functools import cache, lru_cache +from functools import lru_cache from pathlib import Path import gevent from locust import HttpLocust, TaskSet, task - from httprunner.exceptions import MyBaseError, MyBaseFailure -from httprunner.loader import load_dot_env_file from httprunner.task import init_test_suites +from httprunner.loader import load_dot_env_file + sys.path.insert(0, os.path.dirname(__file__)) logging.getLogger().setLevel(logging.CRITICAL) -logging.getLogger("locust.main").setLevel(logging.INFO) -logging.getLogger("locust.runners").setLevel(logging.INFO) +logging.getLogger('locust.main').setLevel(logging.INFO) +logging.getLogger('locust.runners').setLevel(logging.INFO) def P(relpath): @@ -24,52 +25,51 @@ def P(relpath): return abspath.resolve() -@cache +@lru_cache(maxsize=None) def load_tests(): try: - with open(P("testcase.json")) as tj: + with open(P('testcase.json'), 'r') as tj: items = json.load(tj) except FileNotFoundError: return {}, [] try: - load_dot_env_file(P(".env")) + load_dot_env_file(P('.env')) except FileNotFoundError: pass config_obj = {} test_objs = [] for item in items: - if "config" in item: + if 'config' in item: config_obj = item - elif "test" in item: + elif 'test' in item: test_objs.append(item) tests = [] for idx, test_obj in enumerate(test_objs): - name = f"{idx}.json" - with open(P(name), "w") as fp: + name = f'{idx}.json' + with open(P(name), 'w') as fp: json.dump([config_obj, test_obj], fp) - for _ in range(test_obj["test"]["weight"]): + for _ in range(test_obj['test']['weight']): tests.append(name) - config = config_obj["config"] + config = config_obj['config'] return config, tests -@cache +@lru_cache(maxsize=None) def get_work(path, client): suite = init_test_suites(path, None, client) return suite class WebSiteTasks(TaskSet): - """all in one""" - + """all in one + """ def on_start(self): self.variables = {} - for variable in self.locust.hrun_config.get("variables", []): - if "setup_locust" in variable: - setup_func_name = variable["setup_locust"] + for variable in self.locust.hrun_config.get('variables', []): + if 'setup_locust' in variable: + setup_func_name = variable['setup_locust'] try: import debugtalk - setup_func = getattr(debugtalk, setup_func_name) except (AttributeError, ImportError): return @@ -81,20 +81,19 @@ def do_work(self, testfile): work = get_work(testfile, self.locust.client) for suite in work: for test in suite: - test.testcase_dict["variables"].update(self.variables) - test.testcase_dict["request"]["group"] = test.testcase_dict["name"] - test.testcase_dict["request"]["verify"] = False - test.testcase_dict["request"]["timeout"] = 30 + test.testcase_dict['variables'].update(self.variables) + test.testcase_dict['request']['group'] = test.testcase_dict['name'] + test.testcase_dict['request']['verify'] = False + test.testcase_dict['request']['timeout'] = 30 try: test.runTest() except (MyBaseError, MyBaseFailure) as ex: from locust.events import request_failure - request_failure.fire( request_type=test.testcase_dict.get("request", {}).get("method"), name=test.testcase_dict.get("request", {}).get("group"), response_time=0, - exception=ex, + exception=ex ) break gevent.sleep(1) @@ -107,7 +106,7 @@ def test_any(self): class WebSiteUser(HttpLocust): hrun_config, hrun_tests = load_tests() - host = hrun_config.get("request", {}).get("base_url", "") + host = hrun_config.get('request', {}).get('base_url', '') task_set = WebSiteTasks min_wait = 10 max_wait = 10 diff --git a/httprunner/locusts.py b/httprunner/locusts.py index f54a7d6c..829b8e78 100644 --- a/httprunner/locusts.py +++ b/httprunner/locusts.py @@ -1,19 +1,22 @@ +# encoding: utf-8 + import io -import logging import multiprocessing import os import sys +import logging # from httprunner.logger import color_print +from httprunner import loader from locust.main import main -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def parse_locustfile(file_path): - """parse testcase file and return locustfile path. - if file_path is a Python file, assume it is a locustfile - if file_path is a YAML/JSON file, convert it to locustfile + """ parse testcase file and return locustfile path. + if file_path is a Python file, assume it is a locustfile + if file_path is a YAML/JSON file, convert it to locustfile """ if not os.path.isfile(file_path): # color_print("file path invalid, exit.", "RED") @@ -23,7 +26,7 @@ def parse_locustfile(file_path): file_suffix = os.path.splitext(file_path)[1] if file_suffix == ".py": locustfile_path = file_path - elif file_suffix in [".yaml", ".yml", ".json"]: + elif file_suffix in ['.yaml', '.yml', '.json']: locustfile_path = gen_locustfile(file_path) else: # '' or other suffix @@ -32,31 +35,29 @@ def parse_locustfile(file_path): return locustfile_path - def gen_locustfile(testcase_file_path): - """generate locustfile from template.""" - locustfile_path = "locustfile.py" + """ generate locustfile from template. + """ + locustfile_path = 'locustfile.py' template_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "templates", - "locustfile_template", + "locustfile_template" ) - with open(template_path, encoding="utf-8") as template: - with open(locustfile_path, "w", encoding="utf-8") as locustfile: + with io.open(template_path, encoding='utf-8') as template: + with io.open(locustfile_path, 'w', encoding='utf-8') as locustfile: template_content = template.read() template_content = template_content.replace("$TESTCASE_FILE", testcase_file_path) locustfile.write(template_content) return locustfile_path - def start_master(sys_argv): sys_argv.append("--master") sys.argv = sys_argv main() - def start_slave(sys_argv): if "--slave" not in sys_argv: sys_argv.extend(["--slave"]) @@ -64,7 +65,6 @@ def start_slave(sys_argv): sys.argv = sys_argv main() - def run_locusts_with_processes(sys_argv, processes_count): processes = [] manager = multiprocessing.Manager() diff --git a/httprunner/logger.py b/httprunner/logger.py index 02e496ea..889e9cf9 100644 --- a/httprunner/logger.py +++ b/httprunner/logger.py @@ -1,13 +1,19 @@ -from colorama import init +# encoding: utf-8 + +import logging +import sys + +from colorama import Fore, init +from colorlog import ColoredFormatter init(autoreset=True) log_colors_config = { - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red", + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', } # logger = logging.getLogger("httprunner") diff --git a/httprunner/parser.py b/httprunner/parser.py index 24778826..9e4728a2 100644 --- a/httprunner/parser.py +++ b/httprunner/parser.py @@ -1,8 +1,11 @@ +# encoding: utf-8 + import ast import logging import re # from loguru import logger + from httprunner import exceptions, utils from httprunner.compat import basestring, builtin_str, numeric_types, str @@ -18,11 +21,11 @@ function_regex_compile = re.compile(r"\$\{(\w+)\(([\$\w\.\-/\s=,]*)\)\}") -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def parse_string_value(str_value): - """parse string to number if possible + """ parse string to number if possible e.g. "123" => 123 "12.2" => 12.3 "abc" => "abc" @@ -38,7 +41,7 @@ def parse_string_value(str_value): def extract_variables(content): - """extract all variable names from content, which is in format $variable + """ extract all variable names from content, which is in format $variable Args: content (str): string content @@ -68,7 +71,7 @@ def extract_variables(content): def extract_functions(content): - """extract all functions from string content, which are in format ${fun()} + """ extract all functions from string content, which are in format ${fun()} Args: content (str): string content @@ -100,7 +103,7 @@ def extract_functions(content): def parse_function(content): - """parse function name and args from string content. + """ parse function name and args from string content. Args: content (str): string content @@ -133,19 +136,23 @@ def parse_function(content): """ matched = function_regexp_compile.match(content) if not matched: - raise exceptions.FunctionNotFound(f"{content} not found!") + raise exceptions.FunctionNotFound("{} not found!".format(content)) - function_meta = {"func_name": matched.group(1), "args": [], "kwargs": {}} + function_meta = { + "func_name": matched.group(1), + "args": [], + "kwargs": {} + } args_str = matched.group(2).strip() if args_str == "": return function_meta - args_list = args_str.split(",") + args_list = args_str.split(',') for arg in args_list: arg = arg.strip() - if "=" in arg: - key, value = arg.split("=") + if '=' in arg: + key, value = arg.split('=') function_meta["kwargs"][key.strip()] = parse_string_value(value.strip()) else: function_meta["args"].append(parse_string_value(arg)) @@ -154,7 +161,7 @@ def parse_function(content): def parse_validator(validator): - """parse validator, validator maybe in two format + """ parse validator, validator maybe in two format @param (dict) validator format1: this is kept for compatiblity with the previous versions. {"check": "status_code", "comparator": "eq", "expect": 201} @@ -170,7 +177,7 @@ def parse_validator(validator): } """ if not isinstance(validator, dict): - raise exceptions.ParamsError(f"invalid validator: {validator}") + raise exceptions.ParamsError("invalid validator: {}".format(validator)) if "check" in validator and len(validator) > 1: # format1 @@ -181,7 +188,7 @@ def parse_validator(validator): elif "expected" in validator: expect_value = validator.get("expected") else: - raise exceptions.ParamsError(f"invalid validator: {validator}") + raise exceptions.ParamsError("invalid validator: {}".format(validator)) comparator = validator.get("comparator", "eq") @@ -191,32 +198,32 @@ def parse_validator(validator): compare_values: list = validator[comparator] if len(compare_values) == 2: - compare_values.append("") + compare_values.append('') if not isinstance(compare_values, list) or len(compare_values) != 3: - raise exceptions.ParamsError(f"invalid validator: {validator}") + raise exceptions.ParamsError("invalid validator: {}".format(validator)) - if comparator in ("list_any_item_contains", "list_all_item_contains"): + if comparator in ('list_any_item_contains', 'list_all_item_contains'): # list item比较器特殊检查 - if len(compare_values[1].split(" ")) != 3: + if len(compare_values[1].split(' ')) != 3: msg = f"{compare_values} 是错误的表达式, 正确的期望值表达式比如:k = v,等号前后要有空格符" raise exceptions.ExpectValueParseFailure(msg) check_item, expect_value, desc = compare_values else: - raise exceptions.ParamsError(f"invalid validator: {validator}") + raise exceptions.ParamsError("invalid validator: {}".format(validator)) return { "check": check_item, "expect": expect_value, "comparator": comparator, - "desc": desc, + "desc": desc } def substitute_variables(content, variables_mapping): - """substitute variables in content with variables_mapping + """ substitute variables in content with variables_mapping Args: content (str/dict/list/numeric/bool/type): content to be substituted. @@ -243,7 +250,10 @@ def substitute_variables(content, variables_mapping): """ if isinstance(content, (list, set, tuple)): - return [substitute_variables(item, variables_mapping) for item in content] + return [ + substitute_variables(item, variables_mapping) + for item in content + ] if isinstance(content, dict): substituted_data = {} @@ -267,9 +277,8 @@ def substitute_variables(content, variables_mapping): return content - def parse_parameters(parameters, variables_mapping, functions_mapping): - """parse parameters and generate cartesian product. + """ parse parameters and generate cartesian product. Args: parameters (list) parameters: parameter name and value in list @@ -338,7 +347,6 @@ def parse_parameters(parameters, variables_mapping, functions_mapping): ## parse content with variables and functions mapping ############################################################################### - def get_builtin_item(item_type, item_name): """ @@ -352,32 +360,24 @@ def get_builtin_item(item_type, item_name): """ # override built_in module with debugtalk.py module from httprunner import loader - built_in_module = loader.load_builtin_module() if item_type == "variables": try: return built_in_module["variables"][item_name] except KeyError: - logger.error( - "variables_mapping and built_in_module, not found variable: %s", - item_name, - exc_info=True, - ) - raise exceptions.VariableNotFound( - "variables_mapping and built_in_module, not found variable: %s", - item_name, - ) + logger.error("variables_mapping and built_in_module, not found variable: %s", item_name, exc_info=True) + raise exceptions.VariableNotFound("variables_mapping and built_in_module, not found variable: %s", item_name) else: # item_type == "functions": try: return built_in_module["functions"][item_name] except KeyError: - raise exceptions.FunctionNotFound(f"{item_name} is not found.") + raise exceptions.FunctionNotFound("{} is not found.".format(item_name)) def get_mapping_variable(variable_name, variables_mapping): - """get variable from variables_mapping. + """ get variable from variables_mapping. Args: variable_name (str): variable name @@ -397,7 +397,7 @@ def get_mapping_variable(variable_name, variables_mapping): def get_mapping_function(function_name, functions_mapping): - """get function from functions_mapping, + """ get function from functions_mapping, if not found, then try to check if builtin function. Args: @@ -427,11 +427,11 @@ def get_mapping_function(function_name, functions_mapping): return item_func except (NameError, TypeError): # is not builtin function - raise exceptions.FunctionNotFound(f"{function_name} is not found.") + raise exceptions.FunctionNotFound("{} is not found.".format(function_name)) def parse_string_functions(content, variables_mapping, functions_mapping): - """parse string content with functions mapping. + """ parse string content with functions mapping. Args: content (str): string content to be parsed. @@ -460,7 +460,6 @@ def parse_string_functions(content, variables_mapping, functions_mapping): if func_name in ["parameterize", "P"]: from httprunner import loader - eval_value = loader.load_csv_file(*args, **kwargs) else: func = get_mapping_function(func_name, functions_mapping) @@ -472,13 +471,16 @@ def parse_string_functions(content, variables_mapping, functions_mapping): content = eval_value else: # content contains one or many functions, e.g. "abc${add_one(3)}def" - content = content.replace(func_content, str(eval_value), 1) + content = content.replace( + func_content, + str(eval_value), 1 + ) return content def parse_string_variables(content, variables_mapping): - """parse string content with variables mapping. + """ parse string content with variables mapping. Args: content (str): string content to be parsed. @@ -499,7 +501,7 @@ def parse_string_variables(content, variables_mapping): variable_value = get_mapping_variable(variable_name, variables_mapping) # TODO: replace variable label from $var to {{var}} - if f"${variable_name}" == content: + if "${}".format(variable_name) == content: # content is a variable content = variable_value else: @@ -507,13 +509,16 @@ def parse_string_variables(content, variables_mapping): if not isinstance(variable_value, str): variable_value = builtin_str(variable_value) - content = content.replace(f"${variable_name}", variable_value, 1) + content = content.replace( + "${}".format(variable_name), + variable_value, 1 + ) return content def parse_function_params(params) -> dict: - """parse function params to args and kwargs. + """ parse function params to args and kwargs. Args: params (str): function param in string Returns: @@ -557,21 +562,20 @@ def _format_func(func_name, parsed_args, parsed_kwargs): kwargs_str = ",".join(f"{k}={v}" for k, v in parsed_kwargs.items()) if not args_str and not kwargs_str: - return f"{func_name}()" + return f'{func_name}()' elif not args_str: - return f"{func_name}({kwargs_str})" + return f'{func_name}({kwargs_str})' elif not kwargs_str: - return f"{func_name}({args_str})" + return f'{func_name}({args_str})' else: - return f"{func_name}({args_str}, {kwargs_str})" - + return f'{func_name}({args_str}, {kwargs_str})' def parse_string( raw_string, variables_mapping, functions_mapping, ): - """parse string content with variables and functions mapping. + """ parse string content with variables and functions mapping. Args: raw_string: raw string content to be parsed. variables_mapping: variables mapping. @@ -593,6 +597,7 @@ def parse_string( return parsed_string while match_start_position < len(raw_string): + # Notice: notation priority # $$ > ${func($a, $b)} > $var @@ -612,16 +617,13 @@ def parse_string( func_params_str = func_match.group(2) logger.info("raw func_params_str: %s", func_params_str) function_meta = parse_function_params(func_params_str) - logger.info("function_meta: %s", function_meta) + logger.info("function_meta: %s", function_meta) args = function_meta["args"] kwargs = function_meta["kwargs"] parsed_args = parse_data(args, variables_mapping, functions_mapping) parsed_kwargs = parse_data(kwargs, variables_mapping, functions_mapping) try: - logger.info( - "parsed func: %s", - _format_func(func_name, parsed_args, parsed_kwargs), - ) + logger.info('parsed func: %s', _format_func(func_name, parsed_args, parsed_kwargs)) func_eval_value = func(*parsed_args, **parsed_kwargs) logger.info("func return value: %s", func_eval_value) except Exception as ex: @@ -673,9 +675,8 @@ def parse_string( return parsed_string - def parse_data(content, variables_mapping=None, functions_mapping=None): - """parse content with variables mapping + """ parse content with variables mapping Args: content (str/dict/list/numeric/bool/type): content to be parsed @@ -707,7 +708,10 @@ def parse_data(content, variables_mapping=None, functions_mapping=None): return content if isinstance(content, (list, set, tuple)): - return [parse_data(item, variables_mapping, functions_mapping) for item in content] + return [ + parse_data(item, variables_mapping, functions_mapping) + for item in content + ] if isinstance(content, dict): parsed_content = {} @@ -723,10 +727,10 @@ def parse_data(content, variables_mapping=None, functions_mapping=None): functions_mapping = functions_mapping or {} has_variable_match = variable_regex_compile.search(content) if has_variable_match: - logger.info("source string content: %s", content) + logger.info('source string content: %s', content) content = parse_string(content, variables_mapping, functions_mapping) if has_variable_match: - logger.info("parsed string content: %s", content) + logger.info('parsed string content: %s', content) # replace $$ notation with $ and consider it as normal char. # if '$$' in content: # return content.replace("$$", "$") @@ -747,7 +751,7 @@ def parse_data(content, variables_mapping=None, functions_mapping=None): def parse_tests(testcases, variables_mapping=None): - """parse testcases configs, including variables/parameters/name/request. + """ parse testcases configs, including variables/parameters/name/request. Args: testcases (list): testcase list, with config unparsed. @@ -797,11 +801,14 @@ def parse_tests(testcases, variables_mapping=None): project_mapping = testcase_config.pop( "refs", { - "debugtalk": {"variables": {}, "functions": {}}, + "debugtalk": { + "variables": {}, + "functions": {} + }, "env": {}, "def-api": {}, - "def-testcase": {}, - }, + "def-testcase": {} + } ) # parse config parameters @@ -809,7 +816,7 @@ def parse_tests(testcases, variables_mapping=None): cartesian_product_parameters_list = parse_parameters( config_parameters, project_mapping["debugtalk"]["variables"], - project_mapping["debugtalk"]["functions"], + project_mapping["debugtalk"]["functions"] ) or [{}] for parameter_mapping in cartesian_product_parameters_list: @@ -821,16 +828,18 @@ def parse_tests(testcases, variables_mapping=None): parsed_config_variables = parse_data( raw_config_variables, project_mapping["debugtalk"]["variables"], - project_mapping["debugtalk"]["functions"], + project_mapping["debugtalk"]["functions"] ) # priority: passed in > debugtalk.py > parameters > variables # override variables mapping with parameters mapping - config_variables = utils.override_mapping_list(parsed_config_variables, parameter_mapping) + config_variables = utils.override_mapping_list( + parsed_config_variables, parameter_mapping) # merge debugtalk.py module variables config_variables.update(project_mapping["debugtalk"]["variables"]) # override variables mapping with passed in variables_mapping - config_variables = utils.override_mapping_list(config_variables, variables_mapping) + config_variables = utils.override_mapping_list( + config_variables, variables_mapping) testcase_dict["config"]["variables"] = config_variables @@ -838,14 +847,14 @@ def parse_tests(testcases, variables_mapping=None): testcase_dict["config"]["name"] = parse_data( testcase_dict["config"].get("name", ""), config_variables, - project_mapping["debugtalk"]["functions"], + project_mapping["debugtalk"]["functions"] ) # parse config request testcase_dict["config"]["request"] = parse_data( testcase_dict["config"].get("request", {}), config_variables, - project_mapping["debugtalk"]["functions"], + project_mapping["debugtalk"]["functions"] ) # put loaded project functions to config diff --git a/httprunner/report.py b/httprunner/report.py index 7ecac103..22575d7b 100644 --- a/httprunner/report.py +++ b/httprunner/report.py @@ -1,3 +1,5 @@ +# encoding: utf-8 + import io import logging import os @@ -5,7 +7,7 @@ import time import unittest from base64 import b64encode -from collections.abc import Iterable +from typing import Iterable from datetime import datetime from jinja2 import Template, escape @@ -13,13 +15,15 @@ from httprunner.__about__ import __version__ from httprunner.compat import basestring, bytes, json, numeric_types -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def get_platform(): return { "httprunner_version": __version__, - "python_version": "{} {}".format(platform.python_implementation(), platform.python_version()), + "python_version": "{} {}".format( + platform.python_implementation(), platform.python_version() + ), "platform": platform.platform(), } @@ -58,7 +62,7 @@ def get_summary(result): summary["records"] = [] if getattr(result, "vars_trace", None): - summary["vars_trace"]: list[dict] = result.vars_trace + summary["vars_trace"]: list[dict] = result.vars_trace return summary @@ -93,41 +97,45 @@ def render_html_report(summary, html_report_name=None, html_report_template=None ) logger.debug("No html report template specified, use default.") else: - logger.info(f"render with html report template: {html_report_template}") + logger.info("render with html report template: {}".format(html_report_template)) logger.info("Start to render Html report ...") - logger.debug(f"render data: {summary}") + logger.debug("render data: {}".format(summary)) report_dir_path = os.path.join(os.getcwd(), "reports") start_at_timestamp = int(summary["time"]["start_at"]) - summary["time"]["start_datetime"] = datetime.fromtimestamp(start_at_timestamp).strftime("%Y-%m-%d %H:%M:%S") + summary["time"]["start_datetime"] = datetime.fromtimestamp( + start_at_timestamp + ).strftime("%Y-%m-%d %H:%M:%S") if html_report_name: summary["html_report_name"] = html_report_name report_dir_path = os.path.join(report_dir_path, html_report_name) - html_report_name += f"-{start_at_timestamp}.html" + html_report_name += "-{}.html".format(start_at_timestamp) else: summary["html_report_name"] = "" - html_report_name = f"{start_at_timestamp}.html" + html_report_name = "{}.html".format(start_at_timestamp) if not os.path.isdir(report_dir_path): os.makedirs(report_dir_path) for index, suite_summary in enumerate(summary["details"]): if not suite_summary.get("name"): - suite_summary["name"] = f"test suite {index}" + suite_summary["name"] = "test suite {}".format(index) for record in suite_summary.get("records"): meta_data = record["meta_data"] stringify_data(meta_data, "request") stringify_data(meta_data, "response") - with open(html_report_template, encoding="utf-8") as fp_r: + with io.open(html_report_template, "r", encoding="utf-8") as fp_r: template_content = fp_r.read() report_path = os.path.join(report_dir_path, html_report_name) - with open(report_path, "w", encoding="utf-8") as fp_w: - rendered_content = Template(template_content, extensions=["jinja2.ext.loopcontrols"]).render(summary) + with io.open(report_path, "w", encoding="utf-8") as fp_w: + rendered_content = Template( + template_content, extensions=["jinja2.ext.loopcontrols"] + ).render(summary) fp_w.write(rendered_content) - logger.info(f"Generated Html report: {report_path}") + logger.info("Generated Html report: {}".format(report_path)) return report_path @@ -143,6 +151,7 @@ def stringify_data(meta_data, request_or_response): request_or_response_dict = meta_data[request_or_response] for key, value in request_or_response_dict.items(): + if isinstance(value, list): value = json.dumps(value, indent=2, ensure_ascii=False) @@ -247,7 +256,11 @@ def duration(self): if isinstance(elapsed_ms, (int, float)): case_elapsed += record["meta_data"]["response"]["elapsed_ms"] # 毫秒转秒级,保留三位 - total_duration = case_elapsed / 1000 + self.setup_hooks_duration + self.teardown_hooks_duration + total_duration = ( + case_elapsed / 1000 + + self.setup_hooks_duration + + self.teardown_hooks_duration + ) return round(total_duration, 3) @property @@ -255,7 +268,9 @@ def setup_hooks_duration(self): # 整个case的前置函数消耗时间 res = 0 for record in self.records: - setup_hooks_duration = record["meta_data"]["request"].get("setup_hooks_duration") + setup_hooks_duration = record["meta_data"]["request"].get( + "setup_hooks_duration" + ) if setup_hooks_duration: res += setup_hooks_duration return res @@ -265,7 +280,9 @@ def teardown_hooks_duration(self): # 整个case的后置函数消耗时间 res = 0 for record in self.records: - teardown_hooks_duration = record["meta_data"]["response"].get("teardown_hooks_duration") + teardown_hooks_duration = record["meta_data"]["response"].get( + "teardown_hooks_duration" + ) if teardown_hooks_duration: res += teardown_hooks_duration return res diff --git a/httprunner/response.py b/httprunner/response.py index f4e77418..b47e2920 100644 --- a/httprunner/response.py +++ b/httprunner/response.py @@ -1,23 +1,25 @@ +# encoding: utf-8 + import json -import logging import re +import logging -import jsonpath import pydash -from loguru import logger as log - +import jsonpath from httprunner import exceptions, utils +from loguru import logger as log from httprunner.compat import OrderedDict, basestring, is_py2 text_extractor_regexp_compile = re.compile(r".*\(.*\).*") -list_condition_extractor_regexp_compile = re.compile(r"^for#\w+.*#\w.*") +list_condition_extractor_regexp_compile = re.compile(r'^for#\w+.*#\w.*') + +logger = logging.getLogger('httprunner') -logger = logging.getLogger("httprunner") +class ResponseObject(object): -class ResponseObject: def __init__(self, resp_obj): - """initialize with a requests.Response object + """ initialize with a requests.Response object @param (requests.Response instance) resp_obj """ self.resp_obj = resp_obj @@ -32,15 +34,14 @@ def __getattr__(self, key): self.__dict__[key] = value return value except AttributeError: - err_msg = f"ResponseObject does not have attribute: {key}" + err_msg = "ResponseObject does not have attribute: {}".format(key) logger.error(err_msg) raise exceptions.ParamsError(err_msg) - def __str__(self): return self.resp_obj.text def _extract_field_with_regex(self, field): - """extract field from response content with regex. + """ extract field from response content with regex. requests.Response body could be json or html text. @param (str) field should only be regex string that matched r".*\(.*\).*" e.g. @@ -50,15 +51,15 @@ def _extract_field_with_regex(self, field): """ matched = re.search(field, self.text) if not matched: - err_msg = f"Failed to extract data with regex! => {field}\n" - err_msg += f"response body: {self.text}\n" + err_msg = u"Failed to extract data with regex! => {}\n".format(field) + err_msg += u"response body: {}\n".format(self.text) logger.error(err_msg) raise exceptions.ExtractFailure(err_msg) return matched.group(1) def _extract_field_with_delimiter(self, field): - """response content could be json or html text. + """ response content could be json or html text. @param (str) field should be string joined by delimiter. e.g. "status_code" @@ -77,19 +78,19 @@ def _extract_field_with_delimiter(self, field): # e.g. "content.person.name" => ["content", "person.name"] try: - top_query, sub_query = field.split(".", 1) + top_query, sub_query = field.split('.', 1) except ValueError: top_query = field sub_query = None # request - if top_query == "request" and sub_query is not None: + if top_query == 'request' and sub_query is not None: req = self.resp_obj.request - if hasattr(req, "body"): + if hasattr(req, 'body'): body = json.loads(req.body) - if sub_query == "body": + if sub_query == 'body': return body - query_path = sub_query.replace("body.", "", 1) + query_path = sub_query.replace('body.', '', 1) err_msg = f"request body not found: {field}" res = pydash.get(body, query_path, exceptions.ExtractFailure(err_msg)) if isinstance(res, exceptions.ExtractFailure): @@ -100,7 +101,7 @@ def _extract_field_with_delimiter(self, field): if top_query in ["status_code", "encoding", "ok", "reason", "url"]: if sub_query: # status_code.XX - err_msg = f"Failed to extract: {field}\n" + err_msg = u"Failed to extract: {}\n".format(field) logger.error(err_msg) raise exceptions.ParamsError(err_msg) @@ -116,16 +117,16 @@ def _extract_field_with_delimiter(self, field): try: return cookies[sub_query] except KeyError: - err_msg = f"Failed to extract cookie! => {field}\n" - err_msg += f"response cookies: {cookies}\n" + err_msg = u"Failed to extract cookie! => {}\n".format(field) + err_msg += u"response cookies: {}\n".format(cookies) logger.error(err_msg) raise exceptions.ExtractFailure(err_msg) # elapsed elif top_query == "elapsed": - available_attributes = "available attributes: days, seconds, microseconds, total_seconds" + available_attributes = u"available attributes: days, seconds, microseconds, total_seconds" if not sub_query: - err_msg = "elapsed is datetime.timedelta instance, attribute should also be specified!\n" + err_msg = u"elapsed is datetime.timedelta instance, attribute should also be specified!\n" err_msg += available_attributes logger.error(err_msg) raise exceptions.ParamsError(err_msg) @@ -134,7 +135,7 @@ def _extract_field_with_delimiter(self, field): elif sub_query == "total_seconds": return self.elapsed.total_seconds() else: - err_msg = f"{sub_query} is not valid datetime.timedelta attribute.\n" + err_msg = "{} is not valid datetime.timedelta attribute.\n".format(sub_query) err_msg += available_attributes logger.error(err_msg) raise exceptions.ParamsError(err_msg) @@ -149,8 +150,8 @@ def _extract_field_with_delimiter(self, field): try: return headers[sub_query] except KeyError: - err_msg = f"Failed to extract header! => {field}\n" - err_msg += f"response headers: {headers}\n" + err_msg = u"Failed to extract header! => {}\n".format(field) + err_msg += u"response headers: {}\n".format(headers) logger.error(err_msg) raise exceptions.ExtractFailure(err_msg) @@ -176,8 +177,8 @@ def _extract_field_with_delimiter(self, field): return utils.query_json(body, sub_query) else: # content = "abcdefg", content.xxx - err_msg = f"Failed to extract attribute from response body! => {field}\n" - err_msg += f"response body: {body}\n" + err_msg = u"Failed to extract attribute from response body! => {}\n".format(field) + err_msg += u"response body: {}\n".format(body) logger.error(err_msg) raise exceptions.ExtractFailure(err_msg) @@ -197,29 +198,29 @@ def _extract_field_with_delimiter(self, field): return utils.query_json(attributes, sub_query) else: # content = "attributes.new_attribute_not_exist" - err_msg = f"Failed to extract cumstom set attribute from teardown hooks! => {field}\n" - err_msg += f"response set attributes: {attributes}\n" + err_msg = u"Failed to extract cumstom set attribute from teardown hooks! => {}\n".format(field) + err_msg += u"response set attributes: {}\n".format(attributes) logger.error(err_msg) raise exceptions.TeardownHooksFailure(err_msg) # others else: - err_msg = f"Failed to extract attribute from response! => {field}\n" - err_msg += "available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n" - err_msg += "If you want to set attribute in teardown_hooks, take the following example as reference:\n" - err_msg += "response.new_attribute = 'new_attribute_value'\n" + err_msg = u"Failed to extract attribute from response! => {}\n".format(field) + err_msg += u"available response attributes: status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.\n\n" + err_msg += u"If you want to set attribute in teardown_hooks, take the following example as reference:\n" + err_msg += u"response.new_attribute = 'new_attribute_value'\n" logger.error(err_msg) raise exceptions.ParamsError(err_msg) def _extract_with_condition(self, field: str): - """condition extract - for#content.res.list,id==1#content.a + """ condition extract + for#content.res.list,id==1#content.a """ field = field.replace(" ", "") - separator = "#" + separator = '#' keyword, valuepath_and_expression, extract_path = field.split(separator) - if keyword == "for": + if keyword == 'for': try: content = self.json except exceptions.JSONDecodeError: @@ -229,13 +230,13 @@ def _extract_with_condition(self, field: str): condition_list_path, expression = valuepath_and_expression.split(",") # 取值的时候,需要移除content.前缀 - condition_list = pydash.get(content, condition_list_path.replace("content.", "", 1), None) + condition_list = pydash.get(content, condition_list_path.replace('content.', "", 1), None) err_msg = "" if not condition_list: - err_msg = f"抽取条件:{condition_list_path}取值不存在" + err_msg = f'抽取条件:{condition_list_path}取值不存在' elif isinstance(condition_list, list) is False: - err_msg = f"抽取条件的值只能是list类型,实际是{type(condition_list)}" + err_msg = f'抽取条件的值只能是list类型,实际是{type(condition_list)}' if err_msg: log.error(err_msg) @@ -244,7 +245,7 @@ def _extract_with_condition(self, field: str): try: expect_path, expect_value = expression.split("==") except ValueError: - err_msg = "抽取条件的表达式错误,正确写法如:id==1" + err_msg = '抽取条件的表达式错误,正确写法如:id==1' log.error(err_msg) raise exceptions.ExtractFailure(err_msg) @@ -254,14 +255,14 @@ def _extract_with_condition(self, field: str): # 当抽取条件满足时 # 如果抽取路径以content.开头,就从整个json取 # 否则,从当前的对象取 - if extract_path.startswith("content."): - extract_value = pydash.get(content, extract_path.replace("content.", "", 1)) + if extract_path.startswith('content.'): + extract_value = pydash.get(content, extract_path.replace('content.', "", 1)) else: extract_value = pydash.get(d, extract_path) break if not extract_value: - err_msg = "抽取结果不存在" + err_msg = '抽取结果不存在' log.error(err_msg) raise exceptions.ExtractFailure(err_msg) return extract_value @@ -271,20 +272,21 @@ def _extract_with_jsonpath(obj: dict, field: str): path = field.replace("content", "$", 1) res: list = jsonpath.jsonpath(obj, path) if not res: - err_msg = f"Failed to extract attribute from response body! => {field}\n" - err_msg += f"response body: {obj}\n" + err_msg = u"Failed to extract attribute from response body! => {}\n".format(field) + err_msg += u"response body: {}\n".format(obj) raise exceptions.ExtractFailure(err_msg) else: return res[0] def extract_field(self, field): - """extract value from requests.Response.""" + """ extract value from requests.Response. + """ if not isinstance(field, basestring): - err_msg = f"Invalid extractor! => {field}\n" + err_msg = u"Invalid extractor! => {}\n".format(field) logger.error(err_msg) raise exceptions.ParamsError(err_msg) - msg = f"extract: {field}" + msg = "extract: {}".format(field) if text_extractor_regexp_compile.match(field) and field.startswith("content.") is False: value = self._extract_field_with_regex(field) @@ -296,13 +298,13 @@ def extract_field(self, field): if is_py2 and isinstance(value, unicode): value = value.encode("utf-8") - msg += f"\t=> {value}" + msg += "\t=> {}".format(value) logger.debug(msg) return value def extract_response(self, extractors, context): - """extract value from requests.Response and store in OrderedDict. + """ extract value from requests.Response and store in OrderedDict. @param (list) extractors [ {"resp_status_code": "status_code"}, @@ -318,13 +320,10 @@ def extract_response(self, extractors, context): logger.info("start to extract from response object.") extracted_variables_mapping = OrderedDict() extract_binds_order_dict = utils.convert_mappinglist_to_orderdict(extractors) - logger.info("extractors: %s", extract_binds_order_dict) + logger.info("extractors: %s" , extract_binds_order_dict) for key, field in extract_binds_order_dict.items(): - if "$" in field: + if '$' in field: field = context.eval_content(field) extracted_variables_mapping[key] = self.extract_field(field) - logger.info( - "🚀🚀🚀 extract finish, extracted_variables_mapping: %s", - dict(extracted_variables_mapping), - ) + logger.info("🚀🚀🚀 extract finish, extracted_variables_mapping: %s" , dict(extracted_variables_mapping)) return extracted_variables_mapping diff --git a/httprunner/runner.py b/httprunner/runner.py index 089b07fc..b7204d1e 100644 --- a/httprunner/runner.py +++ b/httprunner/runner.py @@ -1,9 +1,10 @@ -import logging +# encoding: utf-8 import threading -import time -from unittest.case import SkipTest +import logging import pydash +import time +from unittest.case import SkipTest from httprunner import exceptions, response, utils from httprunner.client import HttpSession @@ -46,17 +47,15 @@ def _transform_to_list_of_dict(extractors: list[dict], extracted_variables_mappi for key, value in extractor.items(): extract_expr = value actual_value = extracted_variables_mapping[key] - result.append( - { - "output_variable_name": key, - "extract_expr": extract_expr, - "actual_value": actual_value, - } - ) + result.append({ + 'output_variable_name': key, + 'extract_expr': extract_expr, + 'actual_value': actual_value + }) return result -class Runner: +class Runner(object): # 每个线程对应Runner类的实例 instances = {} @@ -128,7 +127,9 @@ def init_test(self, test_dict, level): test_dict = utils.lower_test_dict_keys(test_dict) self.context.init_context_variables(level) - variables = test_dict.get("variables") or test_dict.get("variable_binds", OrderedDict()) + variables = test_dict.get("variables") or test_dict.get( + "variable_binds", OrderedDict() + ) self.context.update_context_variables(variables, level) request_config = test_dict.get("request", {}) @@ -161,12 +162,12 @@ def _handle_skip_feature(self, teststep_dict): elif "skipIf" in teststep_dict: skip_if_condition = teststep_dict["skipIf"] if self.context.eval_content(skip_if_condition): - skip_reason = f"{skip_if_condition} evaluate to True" + skip_reason = "{} evaluate to True".format(skip_if_condition) elif "skipUnless" in teststep_dict: skip_unless_condition = teststep_dict["skipUnless"] if not self.context.eval_content(skip_unless_condition): - skip_reason = f"{skip_unless_condition} evaluate to False" + skip_reason = "{} evaluate to False".format(skip_unless_condition) if skip_reason: raise SkipTest(skip_reason) @@ -221,8 +222,12 @@ def run_test(self, teststep_dict): self._handle_skip_feature(teststep_dict) # prepare - extractors = teststep_dict.get("extract", []) or teststep_dict.get("extractors", []) - validators = teststep_dict.get("validate", []) or teststep_dict.get("validators", []) + extractors = teststep_dict.get("extract", []) or teststep_dict.get( + "extractors", [] + ) + validators = teststep_dict.get("validate", []) or teststep_dict.get( + "validators", [] + ) parsed_request = self.init_test(teststep_dict, level="teststep") self.context.update_teststep_variables_mapping("request", parsed_request) @@ -235,10 +240,14 @@ def run_test(self, teststep_dict): logger.info("execute setup hooks end") # 计算前置setup_hooks消耗的时间 setup_hooks_duration = 0 - self.http_client_session.meta_data["request"]["setup_hooks_start"] = setup_hooks_start + self.http_client_session.meta_data["request"][ + "setup_hooks_start" + ] = setup_hooks_start if len(setup_hooks) > 1: setup_hooks_duration = time.time() - setup_hooks_start - self.http_client_session.meta_data["request"]["setup_hooks_duration"] = setup_hooks_duration + self.http_client_session.meta_data["request"][ + "setup_hooks_duration" + ] = setup_hooks_duration try: url = parsed_request.pop("url") @@ -248,30 +257,24 @@ def run_test(self, teststep_dict): raise exceptions.ParamsError("URL or METHOD missed!") # TODO: move method validation to json schema - valid_methods = [ - "GET", - "HEAD", - "POST", - "PUT", - "PATCH", - "DELETE", - "OPTIONS", - ] + valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] if method.upper() not in valid_methods: - err_msg = f"Invalid HTTP method! => {method}\n" + err_msg = "Invalid HTTP method! => {}\n".format(method) err_msg += "Available HTTP methods: {}".format("/".join(valid_methods)) logger.error(err_msg) raise exceptions.ParamsError(err_msg) - logger.info(f"{method} {url}") - logger.debug(f"request kwargs(raw): {parsed_request}") + logger.info("{method} {url}".format(method=method, url=url)) + logger.debug("request kwargs(raw): {kwargs}".format(kwargs=parsed_request)) user_timeout: str = str(pydash.get(parsed_request, "headers.timeout")) if user_timeout and user_timeout.isdigit(): parsed_request["timeout"] = int(user_timeout) # request - resp = self.http_client_session.request(method, url, name=group_name, **parsed_request) + resp = self.http_client_session.request( + method, url, name=group_name, **parsed_request + ) resp_obj = response.ResponseObject(resp) # teardown hooks @@ -289,14 +292,19 @@ def run_test(self, teststep_dict): self.do_hook_actions(teardown_hooks) teardown_hooks_duration = time.time() - teardown_hooks_start logger.info( - "run teardown hooks end, duration: %s", - teardown_hooks_duration, + "run teardown hooks end, duration: %s", teardown_hooks_duration ) - self.http_client_session.meta_data["response"]["teardown_hooks_start"] = teardown_hooks_start - self.http_client_session.meta_data["response"]["teardown_hooks_duration"] = teardown_hooks_duration + self.http_client_session.meta_data["response"][ + "teardown_hooks_start" + ] = teardown_hooks_start + self.http_client_session.meta_data["response"][ + "teardown_hooks_duration" + ] = teardown_hooks_duration # extract - extracted_variables_mapping = resp_obj.extract_response(extractors, self.context) + extracted_variables_mapping = resp_obj.extract_response( + extractors, self.context + ) self.context.extractors = _transform_to_list_of_dict(extractors, extracted_variables_mapping) logger.info( "source testcase_runtime_variables_mapping: %s", @@ -304,21 +312,18 @@ def run_test(self, teststep_dict): ) logger.info( "source testcase_runtime_variables_mapping update with: %s", - dict(extracted_variables_mapping), + dict(extracted_variables_mapping) + ) + self.context.update_testcase_runtime_variables_mapping( + extracted_variables_mapping ) - self.context.update_testcase_runtime_variables_mapping(extracted_variables_mapping) # validate try: - ( - is_validate_passed, - self.evaluated_validators, - ) = self.context.validate(validators, resp_obj) + is_validate_passed, self.evaluated_validators = self.context.validate(validators, resp_obj) if not is_validate_passed: - fail_validators: list[dict] = [ - v["validate_msg"] for v in self.evaluated_validators if v["validate_msg"] != "ok" - ] - raise exceptions.ValidationFailure("\n".join(fail_validators)) + fail_validators: list[dict] = [v['validate_msg'] for v in self.evaluated_validators if v['validate_msg'] != 'ok'] + raise exceptions.ValidationFailure('\n'.join(fail_validators)) except ( exceptions.ParamsError, exceptions.ExtractFailure, @@ -327,14 +332,14 @@ def run_test(self, teststep_dict): err_req_msg = "request: \n" err_req_msg += "headers: {}\n".format(parsed_request.pop("headers", {})) for k, v in parsed_request.items(): - err_req_msg += f"{k}: {repr(v)}\n" + err_req_msg += "{}: {}\n".format(k, repr(v)) logger.error("❌❌❌ err_req_msg: %s", err_req_msg) # log response err_resp_msg = "response: \n" - err_resp_msg += f"status_code: {resp_obj.status_code}\n" - err_resp_msg += f"headers: {resp_obj.headers}\n" - err_resp_msg += f"body: {repr(resp_obj.text)}\n" + err_resp_msg += "status_code: {}\n".format(resp_obj.status_code) + err_resp_msg += "headers: {}\n".format(resp_obj.headers) + err_resp_msg += "body: {}\n".format(repr(resp_obj.text)) logger.error("❌❌❌ err_resp_msg: %s", err_resp_msg) raise finally: @@ -349,7 +354,9 @@ def extract_output(self, output_variables_list): for variable in output_variables_list: if variable not in variables_mapping: logger.warning( - "variable '{}' can not be found in variables mapping, failed to output!".format(variable) + "variable '{}' can not be found in variables mapping, failed to output!".format( + variable + ) ) continue @@ -358,7 +365,7 @@ def extract_output(self, output_variables_list): return output -class Hrun: +class Hrun(object): """ 特殊关键字,提供给驱动函数中使用 可以在驱动函数中,修改配置变量和用例步骤运行时变量 @@ -383,9 +390,7 @@ def set_config_header(name, value): # 比如: 用例中需要切换账号,实现同时请求头中token和userId current_context = Hrun.get_current_context() pydash.set_( - current_context.TESTCASE_SHARED_REQUEST_MAPPING, - f"headers.{name}", - value, + current_context.TESTCASE_SHARED_REQUEST_MAPPING, f"headers.{name}", value ) @staticmethod diff --git a/httprunner/templates/locustfile.py b/httprunner/templates/locustfile.py index 4d3d0432..859a1c8f 100644 --- a/httprunner/templates/locustfile.py +++ b/httprunner/templates/locustfile.py @@ -1,11 +1,11 @@ import random -from locust import HttpLocust, TaskSet, task -from locust.events import request_failure - +import zmq from httprunner.exceptions import MyBaseError, MyBaseFailure from httprunner.loader import load_locust_tests from httprunner.runner import Runner +from locust import HttpLocust, TaskSet, task +from locust.events import request_failure class WebPageTasks(TaskSet): @@ -24,7 +24,7 @@ def test_any(self): request_type=teststep.get("request", {}).get("method"), name=teststep.get("name"), response_time=0, - exception=ex, + exception=ex ) break gevent.sleep(1) diff --git a/httprunner/utils.py b/httprunner/utils.py index 2b333b47..aadefecc 100644 --- a/httprunner/utils.py +++ b/httprunner/utils.py @@ -1,33 +1,38 @@ +# encoding: utf-8 + import copy import io import itertools import json -import logging import os.path +import string from datetime import datetime +import logging from httprunner import exceptions from httprunner.compat import OrderedDict, basestring, is_py2 -logger = logging.getLogger("httprunner") +logger = logging.getLogger('httprunner') def remove_prefix(text, prefix): - """remove prefix from text""" + """ remove prefix from text + """ if text.startswith(prefix): - return text[len(prefix) :] + return text[len(prefix):] return text def set_os_environ(variables_mapping): - """set variables mapping to os.environ""" + """ set variables mapping to os.environ + """ for variable in variables_mapping: os.environ[variable] = variables_mapping[variable] - logger.debug(f"Loaded variable: {variable}") + logger.debug("Loaded variable: {}".format(variable)) -def query_json(json_content, query, delimiter="."): - """Do an xpath-like query with json_content. +def query_json(json_content, query, delimiter='.'): + """ Do an xpath-like query with json_content. @param (dict/list/string) json_content json_content = { "ids": [1, 2, 3, 4], @@ -47,7 +52,7 @@ def query_json(json_content, query, delimiter="."): @return queried result """ raise_flag = False - response_body = f"response body: {json_content}\n" + response_body = u"response body: {}\n".format(json_content) try: for key in query.split(delimiter): if isinstance(json_content, (list, basestring)): @@ -55,13 +60,14 @@ def query_json(json_content, query, delimiter="."): elif isinstance(json_content, dict): json_content = json_content[key] else: - logger.error(f"invalid type value: {json_content}({type(json_content)})") + logger.error( + "invalid type value: {}({})".format(json_content, type(json_content))) raise_flag = True except (KeyError, ValueError, IndexError): raise_flag = True if raise_flag: - err_msg = f"Failed to extract! => {query}\n" + err_msg = u"Failed to extract! => {}\n".format(query) err_msg += response_body logger.error(err_msg) raise exceptions.ExtractFailure(err_msg) @@ -70,7 +76,8 @@ def query_json(json_content, query, delimiter="."): def get_uniform_comparator(comparator): - """convert comparator alias to uniform name""" + """ convert comparator alias to uniform name + """ if comparator in ["eq", "equals", "==", "is"]: return "equals" elif comparator in ["lt", "less_than"]: @@ -87,40 +94,21 @@ def get_uniform_comparator(comparator): return "string_equals" elif comparator in ["len_eq", "length_equals", "count_eq"]: return "length_equals" - elif comparator in [ - "len_gt", - "count_gt", - "length_greater_than", - "count_greater_than", - ]: + elif comparator in ["len_gt", "count_gt", "length_greater_than", "count_greater_than"]: return "length_greater_than" - elif comparator in [ - "len_ge", - "count_ge", - "length_greater_than_or_equals", - "count_greater_than_or_equals", - ]: + elif comparator in ["len_ge", "count_ge", "length_greater_than_or_equals", \ + "count_greater_than_or_equals"]: return "length_greater_than_or_equals" - elif comparator in [ - "len_lt", - "count_lt", - "length_less_than", - "count_less_than", - ]: + elif comparator in ["len_lt", "count_lt", "length_less_than", "count_less_than"]: return "length_less_than" - elif comparator in [ - "len_le", - "count_le", - "length_less_than_or_equals", - "count_less_than_or_equals", - ]: + elif comparator in ["len_le", "count_le", "length_less_than_or_equals", \ + "count_less_than_or_equals"]: return "length_less_than_or_equals" else: return comparator - def deep_update_dict(origin_dict, override_dict): - """update origin dict with override dict recursively + """ update origin dict with override dict recursively e.g. origin_dict = {'a': 1, 'b': {'c': 2, 'd': 4}} override_dict = {'b': {'c': 3}} return: {'a': 1, 'b': {'c': 3, 'd': 4}} @@ -140,9 +128,8 @@ def deep_update_dict(origin_dict, override_dict): return origin_dict - def lower_dict_keys(origin_dict): - """convert keys in dict to lower case + """ convert keys in dict to lower case Args: origin_dict (dict): mapping data structure @@ -173,13 +160,15 @@ def lower_dict_keys(origin_dict): if not origin_dict or not isinstance(origin_dict, dict): return origin_dict - return {key.lower(): value for key, value in origin_dict.items()} - + return { + key.lower(): value + for key, value in origin_dict.items() + } def lower_test_dict_keys(test_dict): - """convert keys in test_dict to lower case, convertion will occur in two places: - 1, all keys in test_dict; - 2, all keys in test_dict["request"] + """ convert keys in test_dict to lower case, convertion will occur in two places: + 1, all keys in test_dict; + 2, all keys in test_dict["request"] """ # convert keys in test_dict test_dict = lower_dict_keys(test_dict) @@ -190,9 +179,8 @@ def lower_test_dict_keys(test_dict): return test_dict - def convert_mappinglist_to_orderdict(mapping_list): - """convert mapping list to ordered dict + """ convert mapping list to ordered dict Args: mapping_list (list): @@ -219,7 +207,7 @@ def convert_mappinglist_to_orderdict(mapping_list): def deepcopy_dict(data): - """deepcopy dict data, ignore file object (_io.BufferedReader) + """ deepcopy dict data, ignore file object (_io.BufferedReader) Args: data (dict): dict data structure @@ -255,7 +243,7 @@ def deepcopy_dict(data): def update_ordered_dict(ordered_dict, override_mapping): - """override ordered_dict with new mapping. + """ override ordered_dict with new mapping. Args: ordered_dict (OrderDict): original ordered dict @@ -279,7 +267,7 @@ def update_ordered_dict(ordered_dict, override_mapping): def override_mapping_list(variables, new_mapping): - """override variables with new mapping. + """ override variables with new mapping. Args: variables (list): variables list @@ -319,11 +307,14 @@ def override_mapping_list(variables, new_mapping): else: raise exceptions.ParamsError("variables error!") - return update_ordered_dict(variables_ordered_dict, new_mapping) + return update_ordered_dict( + variables_ordered_dict, + new_mapping + ) def get_testcase_io(testcase): - """get testcase input(variables) and output. + """ get testcase input(variables) and output. Args: testcase (unittest.suite.TestSuite): corresponding to one YAML/JSON file, it has been set two attributes: @@ -338,11 +329,14 @@ def get_testcase_io(testcase): variables = testcase.config.get("variables", []) output_list = testcase.config.get("output", []) - return {"in": dict(variables), "out": runner.extract_output(output_list)} + return { + "in": dict(variables), + "out": runner.extract_output(output_list) + } def print_io(in_out): - """print input(variables) and output. + """ print input(variables) and output. Args: in_out (dict): input(variables) and output mapping. @@ -387,7 +381,7 @@ def prepare_content(var_type, in_out): if isinstance(value, unicode): value = value.encode("utf-8") if value is None: - value = "None" + value = 'None' content += content_format.format(var_type, variable, value) return content @@ -404,21 +398,22 @@ def prepare_content(var_type, in_out): def create_scaffold(project_name): - """create scaffold with specified project name.""" + """ create scaffold with specified project name. + """ if os.path.isdir(project_name): - logger.warning(f"Folder {project_name} exists, please specify a new folder name.") + logger.warning(u"Folder {} exists, please specify a new folder name.".format(project_name)) return - logger.info(f"Start to create new project: {project_name}", "GREEN") - logger.info(f"CWD: {os.getcwd()}\n", "BLUE") + logger.info("Start to create new project: {}".format(project_name), "GREEN") + logger.info("CWD: {}\n".format(os.getcwd()), "BLUE") def create_path(path, ptype): if ptype == "folder": os.makedirs(path) elif ptype == "file": - open(path, "w").close() + open(path, 'w').close() - msg = f"created {ptype}: {path}" + msg = "created {}: {}".format(ptype, path) logger.info(msg, "BLUE") path_list = [ @@ -428,13 +423,13 @@ def create_path(path, ptype): (os.path.join(project_name, "testsuites"), "folder"), (os.path.join(project_name, "reports"), "folder"), (os.path.join(project_name, "debugtalk.py"), "file"), - (os.path.join(project_name, ".env"), "file"), + (os.path.join(project_name, ".env"), "file") ] [create_path(p[0], p[1]) for p in path_list] def gen_cartesian_product(*args): - """generate cartesian product for lists + """ generate cartesian product for lists @param (list) args [{"a": 1}, {"a": 2}], @@ -468,15 +463,16 @@ def gen_cartesian_product(*args): def validate_json_file(file_list): - """validate JSON testcase format""" + """ validate JSON testcase format + """ for json_file in set(file_list): if not json_file.endswith(".json"): - logger.warning(f"Only JSON file format can be validated, skip: {json_file}") + logger.warning("Only JSON file format can be validated, skip: {}".format(json_file)) continue - logger.info(f"Start to validate JSON file: {json_file}", "GREEN") + logger.info("Start to validate JSON file: {}".format(json_file), "GREEN") - with open(json_file) as stream: + with io.open(json_file) as stream: try: json.load(stream) except ValueError as e: @@ -486,29 +482,30 @@ def validate_json_file(file_list): def prettify_json_file(file_list): - """prettify JSON testcase format""" + """ prettify JSON testcase format + """ for json_file in set(file_list): if not json_file.endswith(".json"): - logger.warning(f"Only JSON file format can be prettified, skip: {json_file}") + logger.warning("Only JSON file format can be prettified, skip: {}".format(json_file)) continue - logger.info(f"Start to prettify JSON file: {json_file}", "GREEN") + logger.info("Start to prettify JSON file: {}".format(json_file), "GREEN") dir_path = os.path.dirname(json_file) file_name, file_suffix = os.path.splitext(os.path.basename(json_file)) - outfile = os.path.join(dir_path, f"{file_name}.pretty.json") + outfile = os.path.join(dir_path, "{}.pretty.json".format(file_name)) - with open(json_file, encoding="utf-8") as stream: + with io.open(json_file, 'r', encoding='utf-8') as stream: try: obj = json.load(stream) except ValueError as e: raise SystemExit(e) - with open(outfile, "w", encoding="utf-8") as out: - json.dump(obj, out, indent=4, separators=(",", ": ")) - out.write("\n") + with io.open(outfile, 'w', encoding='utf-8') as out: + json.dump(obj, out, indent=4, separators=(',', ': ')) + out.write('\n') - print(f"success: {outfile}") + print("success: {}".format(outfile)) def get_python2_retire_msg(): diff --git a/httprunner/validator.py b/httprunner/validator.py index 41ea75ee..b4c8601e 100644 --- a/httprunner/validator.py +++ b/httprunner/validator.py @@ -1,13 +1,14 @@ +# encoding: utf-8 import os import types + """ validate data format TODO: refactor with JSON schema validate """ - def is_testcase(data_structure): - """check if data_structure is a testcase. + """ check if data_structure is a testcase. Args: data_structure (dict): testcase should always be in the following data structure: @@ -49,7 +50,7 @@ def is_testcase(data_structure): def is_testcases(data_structure): - """check if data_structure is testcase or testcases list. + """ check if data_structure is testcase or testcases list. Args: data_structure (dict): testcase(s) should always be in the following data structure: @@ -75,7 +76,7 @@ def is_testcases(data_structure): def is_testcase_path(path): - """check if path is testcase path or path list. + """ check if path is testcase path or path list. Args: path (str/list): file path or file path list. @@ -105,13 +106,15 @@ def is_testcase_path(path): def is_function(tup): - """Takes (name, object) tuple, returns True if it is a function.""" + """ Takes (name, object) tuple, returns True if it is a function. + """ name, item = tup return isinstance(item, types.FunctionType) def is_variable(tup): - """Takes (name, object) tuple, returns True if it is a variable.""" + """ Takes (name, object) tuple, returns True if it is a variable. + """ name, item = tup if callable(item): # function or class diff --git a/manage.py b/manage.py index c28a907c..492440c2 100644 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FasterRunner.settings") + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FasterRunner.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -12,4 +12,4 @@ "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc - execute_from_command_line(sys.argv) + execute_from_command_line(sys.argv) \ No newline at end of file diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index aa86e19c..00000000 --- a/poetry.lock +++ /dev/null @@ -1,1733 +0,0 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. - -[[package]] -name = "aiocontextvars" -version = "0.2.2" -description = "Asyncio support for PEP-567 contextvars backport." -optional = false -python-versions = ">=3.5" -files = [ - {file = "aiocontextvars-0.2.2-py2.py3-none-any.whl", hash = "sha256:885daf8261818767d8f7cbd79f9d4482d118f024b6586ef6e67980236a27bfa3"}, - {file = "aiocontextvars-0.2.2.tar.gz", hash = "sha256:f027372dc48641f683c559f247bd84962becaacdc9ba711d583c3871fb5652aa"}, -] - -[[package]] -name = "amqp" -version = "5.2.0" -description = "Low-level AMQP client for Python (fork of amqplib)." -optional = false -python-versions = ">=3.6" -files = [ - {file = "amqp-5.2.0-py3-none-any.whl", hash = "sha256:827cb12fb0baa892aad844fd95258143bce4027fdac4fccddbc43330fd281637"}, - {file = "amqp-5.2.0.tar.gz", hash = "sha256:a1ecff425ad063ad42a486c902807d1482311481c8ad95a72694b2975e75f7fd"}, -] - -[package.dependencies] -vine = ">=5.0.0,<6.0.0" - -[[package]] -name = "asgiref" -version = "3.7.2" -description = "ASGI specs, helper code, and adapters" -optional = false -python-versions = ">=3.7" -files = [ - {file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"}, - {file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"}, -] - -[package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] - -[[package]] -name = "beautifulsoup4" -version = "4.6.3" -description = "Screen-scraping library" -optional = false -python-versions = "*" -files = [ - {file = "beautifulsoup4-4.6.3-py2-none-any.whl", hash = "sha256:f0abd31228055d698bb392a826528ea08ebb9959e6bea17c606fd9c9009db938"}, - {file = "beautifulsoup4-4.6.3-py3-none-any.whl", hash = "sha256:194ec62a25438adcb3fdb06378b26559eda1ea8a747367d34c33cef9c7f48d57"}, - {file = "beautifulsoup4-4.6.3.tar.gz", hash = "sha256:90f8e61121d6ae58362ce3bed8cd997efb00c914eae0ff3d363c32f9a9822d10"}, -] - -[package.extras] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "billiard" -version = "3.6.4.0" -description = "Python multiprocessing fork with improvements and bugfixes" -optional = false -python-versions = "*" -files = [ - {file = "billiard-3.6.4.0-py3-none-any.whl", hash = "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"}, - {file = "billiard-3.6.4.0.tar.gz", hash = "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547"}, -] - -[[package]] -name = "celery" -version = "5.2.7" -description = "Distributed Task Queue." -optional = false -python-versions = ">=3.7" -files = [ - {file = "celery-5.2.7-py3-none-any.whl", hash = "sha256:138420c020cd58d6707e6257b6beda91fd39af7afde5d36c6334d175302c0e14"}, - {file = "celery-5.2.7.tar.gz", hash = "sha256:fafbd82934d30f8a004f81e8f7a062e31413a23d444be8ee3326553915958c6d"}, -] - -[package.dependencies] -billiard = ">=3.6.4.0,<4.0" -click = ">=8.0.3,<9.0" -click-didyoumean = ">=0.0.3" -click-plugins = ">=1.1.1" -click-repl = ">=0.2.0" -kombu = ">=5.2.3,<6.0" -pytz = ">=2021.3" -vine = ">=5.0.0,<6.0" - -[package.extras] -arangodb = ["pyArango (>=1.3.2)"] -auth = ["cryptography"] -azureblockblob = ["azure-storage-blob (==12.9.0)"] -brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] -cassandra = ["cassandra-driver (<3.21.0)"] -consul = ["python-consul2"] -cosmosdbsql = ["pydocumentdb (==2.3.2)"] -couchbase = ["couchbase (>=3.0.0)"] -couchdb = ["pycouchdb"] -django = ["Django (>=1.11)"] -dynamodb = ["boto3 (>=1.9.178)"] -elasticsearch = ["elasticsearch"] -eventlet = ["eventlet (>=0.32.0)"] -gevent = ["gevent (>=1.5.0)"] -librabbitmq = ["librabbitmq (>=1.5.0)"] -memcache = ["pylibmc"] -mongodb = ["pymongo[srv] (>=3.11.1)"] -msgpack = ["msgpack"] -pymemcache = ["python-memcached"] -pyro = ["pyro4"] -pytest = ["pytest-celery"] -redis = ["redis (>=3.4.1,!=4.0.0,!=4.0.1)"] -s3 = ["boto3 (>=1.9.125)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -solar = ["ephem"] -sqlalchemy = ["sqlalchemy"] -sqs = ["kombu[sqs]"] -tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=1.3.1)"] -zstd = ["zstandard"] - -[[package]] -name = "certifi" -version = "2019.9.11" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = "*" -files = [ - {file = "certifi-2019.9.11-py2.py3-none-any.whl", hash = "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef"}, - {file = "certifi-2019.9.11.tar.gz", hash = "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50"}, -] - -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "chardet" -version = "3.0.4" -description = "Universal encoding detector for Python 2 and 3" -optional = false -python-versions = "*" -files = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "click-didyoumean" -version = "0.3.0" -description = "Enables git-like *did-you-mean* feature in click" -optional = false -python-versions = ">=3.6.2,<4.0.0" -files = [ - {file = "click-didyoumean-0.3.0.tar.gz", hash = "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"}, - {file = "click_didyoumean-0.3.0-py3-none-any.whl", hash = "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667"}, -] - -[package.dependencies] -click = ">=7" - -[[package]] -name = "click-plugins" -version = "1.1.1" -description = "An extension module for click to enable registering CLI commands via setuptools entry-points." -optional = false -python-versions = "*" -files = [ - {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, - {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, -] - -[package.dependencies] -click = ">=4.0" - -[package.extras] -dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] - -[[package]] -name = "click-repl" -version = "0.3.0" -description = "REPL plugin for Click" -optional = false -python-versions = ">=3.6" -files = [ - {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, - {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, -] - -[package.dependencies] -click = ">=7.0" -prompt-toolkit = ">=3.0.36" - -[package.extras] -testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] - -[[package]] -name = "colorama" -version = "0.4.1" -description = "Cross-platform colored terminal text." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "colorama-0.4.1-py2.py3-none-any.whl", hash = "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"}, - {file = "colorama-0.4.1.tar.gz", hash = "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d"}, -] - -[[package]] -name = "colorlog" -version = "4.0.2" -description = "Log formatting with colors!" -optional = false -python-versions = "*" -files = [ - {file = "colorlog-4.0.2-py2.py3-none-any.whl", hash = "sha256:450f52ea2a2b6ebb308f034ea9a9b15cea51e65650593dca1da3eb792e4e4981"}, - {file = "colorlog-4.0.2.tar.gz", hash = "sha256:3cf31b25cbc8f86ec01fef582ef3b840950dea414084ed19ab922c8b493f9b42"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} - -[[package]] -name = "contextvars" -version = "2.4" -description = "PEP 567 Backport" -optional = false -python-versions = "*" -files = [ - {file = "contextvars-2.4.tar.gz", hash = "sha256:f38c908aaa59c14335eeea12abea5f443646216c4e29380d7bf34d2018e2c39e"}, -] - -[package.dependencies] -immutables = ">=0.9" - -[[package]] -name = "coreapi" -version = "2.3.3" -description = "Python client library for Core API." -optional = false -python-versions = "*" -files = [ - {file = "coreapi-2.3.3-py2.py3-none-any.whl", hash = "sha256:bf39d118d6d3e171f10df9ede5666f63ad80bba9a29a8ec17726a66cf52ee6f3"}, - {file = "coreapi-2.3.3.tar.gz", hash = "sha256:46145fcc1f7017c076a2ef684969b641d18a2991051fddec9458ad3f78ffc1cb"}, -] - -[package.dependencies] -coreschema = "*" -itypes = "*" -requests = "*" -uritemplate = "*" - -[[package]] -name = "coreschema" -version = "0.0.4" -description = "Core Schema." -optional = false -python-versions = "*" -files = [ - {file = "coreschema-0.0.4-py2-none-any.whl", hash = "sha256:5e6ef7bf38c1525d5e55a895934ab4273548629f16aed5c0a6caa74ebf45551f"}, - {file = "coreschema-0.0.4.tar.gz", hash = "sha256:9503506007d482ab0867ba14724b93c18a33b22b6d19fb419ef2d239dd4a1607"}, -] - -[package.dependencies] -jinja2 = "*" - -[[package]] -name = "cron-descriptor" -version = "1.4.3" -description = "A Python library that converts cron expressions into human readable strings." -optional = false -python-versions = "*" -files = [ - {file = "cron_descriptor-1.4.3-py3-none-any.whl", hash = "sha256:a67ba21804983b1427ed7f3e1ec27ee77bf24c652b0430239c268c5ddfbf9dc0"}, - {file = "cron_descriptor-1.4.3.tar.gz", hash = "sha256:7b1a00d7d25d6ae6896c0da4457e790b98cba778398a3d48e341e5e0d33f0488"}, -] - -[package.extras] -dev = ["polib"] - -[[package]] -name = "croniter" -version = "1.3.15" -description = "croniter provides iteration for datetime object with cron like format" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "croniter-1.3.15-py2.py3-none-any.whl", hash = "sha256:f17f877be1d93b9e3191151584a19d8b367b017ab0febc8c5472b9300da61c4c"}, - {file = "croniter-1.3.15.tar.gz", hash = "sha256:924a38fda88f675ec6835667e1d32ac37ff0d65509c2152729d16ff205e32a65"}, -] - -[package.dependencies] -python-dateutil = "*" - -[[package]] -name = "curlify" -version = "2.2.1" -description = "Library to convert python requests object to curl command." -optional = false -python-versions = "*" -files = [ - {file = "curlify-2.2.1.tar.gz", hash = "sha256:0d3f02e7235faf952de8ef45ef469845196d30632d5838bcd5aee217726ddd6d"}, -] - -[package.dependencies] -requests = "*" - -[[package]] -name = "dingtalkchatbot" -version = "1.3.0" -description = "一个钉钉自定义机器人消息的Python封装库" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -files = [ - {file = "DingtalkChatbot-1.3.0-py2.py3-none-any.whl", hash = "sha256:cb1eba079f80682c6754062c575c8466dd79e40066218f56a39a28a725abfd83"}, - {file = "DingtalkChatbot-1.3.0.tar.gz", hash = "sha256:83ad216a3c8255906fde4b1ca733490f5b944efdb872c65bc705b0de8ebd6b21"}, -] - -[package.dependencies] -requests = "*" - -[[package]] -name = "django" -version = "4.1.13" -description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -optional = false -python-versions = ">=3.8" -files = [ - {file = "Django-4.1.13-py3-none-any.whl", hash = "sha256:04ab3f6f46d084a0bba5a2c9a93a3a2eb3fe81589512367a75f79ee8acf790ce"}, - {file = "Django-4.1.13.tar.gz", hash = "sha256:94a3f471e833c8f124ee7a2de11e92f633991d975e3fa5bdd91e8abd66426318"}, -] - -[package.dependencies] -asgiref = ">=3.5.2,<4" -sqlparse = ">=0.2.2" -tzdata = {version = "*", markers = "sys_platform == \"win32\""} - -[package.extras] -argon2 = ["argon2-cffi (>=19.1.0)"] -bcrypt = ["bcrypt"] - -[[package]] -name = "django-auth-ldap" -version = "2.3.0" -description = "Django LDAP authentication backend." -optional = false -python-versions = ">=3.6" -files = [ - {file = "django-auth-ldap-2.3.0.tar.gz", hash = "sha256:5894317122a086c9955ed366562869a81459cf6b663636b152857bb5d3a0a3b7"}, - {file = "django_auth_ldap-2.3.0-py3-none-any.whl", hash = "sha256:cbbb476eff2504b5ab4fdf1fa92d93d2d3408fd9c8bc0c426169d987d0733153"}, -] - -[package.dependencies] -Django = ">=2.2" -python-ldap = ">=3.1" - -[[package]] -name = "django-bulk-update" -version = "2.2.0" -description = "Bulk update using one query over Django ORM." -optional = false -python-versions = "*" -files = [ - {file = "django-bulk-update-2.2.0.tar.gz", hash = "sha256:5ab7ce8a65eac26d19143cc189c0f041d5c03b9d1b290ca240dc4f3d6aaeb337"}, - {file = "django_bulk_update-2.2.0-py2.py3-none-any.whl", hash = "sha256:49a403392ae05ea872494d74fb3dfa3515f8df5c07cc277c3dc94724c0ee6985"}, -] - -[package.dependencies] -Django = ">=1.8" - -[[package]] -name = "django-celery-beat" -version = "2.5.0" -description = "Database-backed Periodic Tasks." -optional = false -python-versions = "*" -files = [ - {file = "django-celery-beat-2.5.0.tar.gz", hash = "sha256:cd0a47f5958402f51ac0c715bc942ae33d7b50b4e48cba91bc3f2712be505df1"}, - {file = "django_celery_beat-2.5.0-py3-none-any.whl", hash = "sha256:ae460faa5ea142fba0875409095d22f6bd7bcc7377889b85e8cab5c0dfb781fe"}, -] - -[package.dependencies] -celery = ">=5.2.3,<6.0" -cron-descriptor = ">=1.2.32" -Django = ">=2.2,<5.0" -django-timezone-field = ">=5.0" -python-crontab = ">=2.3.4" -tzdata = "*" - -[[package]] -name = "django-cors-headers" -version = "4.3.1" -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.1.tar.gz", hash = "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207"}, - {file = "django_cors_headers-4.3.1-py3-none-any.whl", hash = "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36"}, -] - -[package.dependencies] -asgiref = ">=3.6" -Django = ">=3.2" - -[[package]] -name = "django-filter" -version = "2.4.0" -description = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." -optional = false -python-versions = ">=3.5" -files = [ - {file = "django-filter-2.4.0.tar.gz", hash = "sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06"}, - {file = "django_filter-2.4.0-py3-none-any.whl", hash = "sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"}, -] - -[package.dependencies] -Django = ">=2.2" - -[[package]] -name = "django-jsonfield" -version = "1.4.0" -description = "JSONField for django models" -optional = false -python-versions = "*" -files = [ - {file = "django-jsonfield-1.4.0.tar.gz", hash = "sha256:9b3dac9f7352a6d37e9cfe0126c5b58ac7abf1cb20c0da294a00269a725223f1"}, - {file = "django_jsonfield-1.4.0-py2.py3-none-any.whl", hash = "sha256:7bdd0ea75ad842b9e33decdf343398c91fbb7bd664fde0648ef83e78b0453b6e"}, -] - -[package.dependencies] -Django = ">=1.11" -six = "*" - -[[package]] -name = "django-log-request-id" -version = "2.0.0" -description = "Django middleware and log filter to attach a unique ID to every log message generated as part of a request" -optional = false -python-versions = "*" -files = [ - {file = "django-log-request-id-2.0.0.tar.gz", hash = "sha256:bccdabcdf536345e7ae40562d4b0fcdc230e0ebe1a8b65ab4a1ed9d3083cf870"}, - {file = "django_log_request_id-2.0.0-py3-none-any.whl", hash = "sha256:8efb8a850fb631b59924cf6fcc8aefe707b0a7dcad42de55284ce807c31b7bc1"}, -] - -[package.dependencies] -django = ">=1.8" - -[[package]] -name = "django-model-utils" -version = "4.0.0" -description = "Django model mixins and utilities" -optional = false -python-versions = "*" -files = [ - {file = "django-model-utils-4.0.0.tar.gz", hash = "sha256:adf09e5be15122a7f4e372cb5a6dd512bbf8d78a23a90770ad0983ee9d909061"}, - {file = "django_model_utils-4.0.0-py2.py3-none-any.whl", hash = "sha256:9cf882e5b604421b62dbe57ad2b18464dc9c8f963fc3f9831badccae66c1139c"}, -] - -[package.dependencies] -Django = ">=2.0.1" - -[[package]] -name = "django-mysql" -version = "4.12.0" -description = "Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases." -optional = false -python-versions = ">=3.8" -files = [ - {file = "django_mysql-4.12.0-py3-none-any.whl", hash = "sha256:1c188ee3a92590da21ce23351c309bb02df3b6611926521d312a9cdf6373c3d0"}, - {file = "django_mysql-4.12.0.tar.gz", hash = "sha256:9a29b69ad30c85362984903379783b53731ee7b0cef4dfb4eb078f80e24f26d4"}, -] - -[package.dependencies] -Django = ">=3.2" - -[[package]] -name = "django-rest-swagger" -version = "2.2.0" -description = "Swagger UI for Django REST Framework 3.5+" -optional = false -python-versions = "*" -files = [ - {file = "django-rest-swagger-2.2.0.tar.gz", hash = "sha256:48f6aded9937e90ae7cbe9e6c932b9744b8af80cc4e010088b3278c700e0685b"}, - {file = "django_rest_swagger-2.2.0-py2.py3-none-any.whl", hash = "sha256:b039b0288bab4665cd45dc5d16f94b13911bc4ad0ed55f74ad3b90aa31c87c17"}, -] - -[package.dependencies] -coreapi = ">=2.3.0" -djangorestframework = ">=3.5.4" -openapi-codec = ">=1.3.1" -simplejson = "*" - -[[package]] -name = "django-simpleui" -version = "2023.12.12" -description = "django admin theme 后台模板" -optional = false -python-versions = "*" -files = [ - {file = "django-simpleui-2023.12.12.tar.gz", hash = "sha256:517f0cd6cf657aac89de4b6b48b929c817a7bcb428779bbd80fd5213766e2518"}, -] - -[package.dependencies] -django = "*" - -[[package]] -name = "django-timezone-field" -version = "6.1.0" -description = "A Django app providing DB, form, and REST framework fields for zoneinfo and pytz timezone objects." -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "django_timezone_field-6.1.0-py3-none-any.whl", hash = "sha256:0095f43da716552fcc606783cfb42cb025892514f1ec660ebfa96186eb83b74c"}, - {file = "django_timezone_field-6.1.0.tar.gz", hash = "sha256:d40f7059d7bae4075725d04a9dae601af9fe3c7f0119a69b0e2c6194a782f797"}, -] - -[package.dependencies] -Django = ">=3.2,<6.0" - -[[package]] -name = "django-utils-six" -version = "2.0" -description = "Forward compatibility django.utils.six for Django 3" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "django-utils-six-2.0.tar.gz", hash = "sha256:4ef9d20de679a5b2448429476452493661fe08f23d6e788a8e9816ec05e3c5b0"}, - {file = "django_utils_six-2.0-py3-none-any.whl", hash = "sha256:8f0e77289d911069a6a15bab5a25ccf2c0fcb8fc4177461dd17470cfdeb94aa6"}, -] - -[[package]] -name = "djangorestframework" -version = "3.14.0" -description = "Web APIs for Django, made easy." -optional = false -python-versions = ">=3.6" -files = [ - {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"}, - {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"}, -] - -[package.dependencies] -django = ">=3.0" -pytz = "*" - -[[package]] -name = "djangorestframework-jwt" -version = "1.11.0" -description = "JSON Web Token based authentication for Django REST framework" -optional = false -python-versions = "*" -files = [ - {file = "djangorestframework-jwt-1.11.0.tar.gz", hash = "sha256:5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7"}, - {file = "djangorestframework_jwt-1.11.0-py2.py3-none-any.whl", hash = "sha256:ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c"}, -] - -[package.dependencies] -PyJWT = ">=1.5.2,<2.0.0" - -[[package]] -name = "drf-yasg" -version = "1.21.7" -description = "Automated generation of real Swagger/OpenAPI 2.0 schemas from Django Rest Framework code." -optional = false -python-versions = ">=3.6" -files = [ - {file = "drf-yasg-1.21.7.tar.gz", hash = "sha256:4c3b93068b3dfca6969ab111155e4dd6f7b2d680b98778de8fd460b7837bdb0d"}, - {file = "drf_yasg-1.21.7-py3-none-any.whl", hash = "sha256:f85642072c35e684356475781b7ecf5d218fff2c6185c040664dd49f0a4be181"}, -] - -[package.dependencies] -django = ">=2.2.16" -djangorestframework = ">=3.10.3" -inflection = ">=0.3.1" -packaging = ">=21.0" -pytz = ">=2021.1" -pyyaml = ">=5.1" -uritemplate = ">=3.0.0" - -[package.extras] -coreapi = ["coreapi (>=2.3.3)", "coreschema (>=0.0.4)"] -validation = ["swagger-spec-validator (>=2.1.0)"] - -[[package]] -name = "faker" -version = "7.0.1" -description = "Faker is a Python package that generates fake data for you." -optional = false -python-versions = ">=3.6" -files = [ - {file = "Faker-7.0.1-py3-none-any.whl", hash = "sha256:08c4cfbfd498c0e90aff6741771c01803d894013df858db6a573182c6a47951f"}, - {file = "Faker-7.0.1.tar.gz", hash = "sha256:20c6e4253b73ef2a783d38e085e7c8d8916295fff31c7403116d2af8f908f7ca"}, -] - -[package.dependencies] -python-dateutil = ">=2.4" -text-unidecode = "1.3" - -[[package]] -name = "genson" -version = "1.2.2" -description = "GenSON is a powerful, user-friendly JSON Schema generator." -optional = false -python-versions = "*" -files = [ - {file = "genson-1.2.2.tar.gz", hash = "sha256:8caf69aa10af7aee0e1a1351d1d06801f4696e005f06cedef438635384346a16"}, -] - -[[package]] -name = "gevent" -version = "22.10.2" -description = "Coroutine-based network library" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5" -files = [ - {file = "gevent-22.10.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:97cd42382421779f5d82ec5007199e8a84aa288114975429e4fd0a98f2290f10"}, - {file = "gevent-22.10.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:1e1286a76f15b5e15f1e898731d50529e249529095a032453f2c101af3fde71c"}, - {file = "gevent-22.10.2-cp27-cp27m-win32.whl", hash = "sha256:59b47e81b399d49a5622f0f503c59f1ce57b7705306ea0196818951dfc2f36c8"}, - {file = "gevent-22.10.2-cp27-cp27m-win_amd64.whl", hash = "sha256:1d543c9407a1e4bca11a8932916988cfb16de00366de5bf7bc9e7a3f61e60b18"}, - {file = "gevent-22.10.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4e2f008c82dc54ec94f4de12ca6feea60e419babb48ec145456907ae61625aa4"}, - {file = "gevent-22.10.2-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:990d7069f14dc40674e0d5cb43c68fd3bad8337048613b9bb94a0c4180ffc176"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f23d0997149a816a2a9045af29c66f67f405a221745b34cefeac5769ed451db8"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b43d500d7d3c0e03070dee813335bb5315215aa1cf6a04c61093dfdd718640b3"}, - {file = "gevent-22.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b68f4c9e20e47ad49fe797f37f91d5bbeace8765ce2707f979a8d4ec197e4d"}, - {file = "gevent-22.10.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1f001cac0ba8da76abfeb392a3057f81fab3d67cc916c7df8ea977a44a2cc989"}, - {file = "gevent-22.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:3b7eae8a0653ba95a224faaddf629a913ace408edb67384d3117acf42d7dcf89"}, - {file = "gevent-22.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8f2477e7b0a903a01485c55bacf2089110e5f767014967ba4b287ff390ae2638"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ddaa3e310a8f1a45b5c42cf50b54c31003a3028e7d4e085059090ea0e7a5fddd"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98bc510e80f45486ef5b806a1c305e0e89f0430688c14984b0dbdec03331f48b"}, - {file = "gevent-22.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:877abdb3a669576b1d51ce6a49b7260b2a96f6b2424eb93287e779a3219d20ba"}, - {file = "gevent-22.10.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d21ad79cca234cdbfa249e727500b0ddcbc7adfff6614a96e6eaa49faca3e4f2"}, - {file = "gevent-22.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e955238f59b2947631c9782a713280dd75884e40e455313b5b6bbc20b92ff73"}, - {file = "gevent-22.10.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:5aa99e4882a9e909b4756ee799c6fa0f79eb0542779fad4cc60efa23ec1b2aa8"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:d82081656a5b9a94d37c718c8646c757e1617e389cdc533ea5e6a6f0b8b78545"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f4bfd74c178351a4a05c5c7df6f8a0a279ff6f392b57608ce0e83c768207f9"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ff3796692dff50fec2f381b9152438b221335f557c4f9b811f7ded51b7a25a1"}, - {file = "gevent-22.10.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f01c9adbcb605364694b11dcd0542ec468a29ac7aba2fb5665dc6caf17ba4d7e"}, - {file = "gevent-22.10.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9d85574eb729f981fea9a78998725a06292d90a3ed50ddca74530c3148c0be41"}, - {file = "gevent-22.10.2-cp36-cp36m-win32.whl", hash = "sha256:8c192d2073e558e241f0b592c1e2b34127a4481a5be240cad4796533b88b1a98"}, - {file = "gevent-22.10.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a2237451c721a0f874ef89dbb4af4fdc172b76a964befaa69deb15b8fff10f49"}, - {file = "gevent-22.10.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:53ee7f170ed42c7561fe8aff5d381dc9a4124694e70580d0c02fba6aafc0ea37"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:96c56c280e3c43cfd075efd10b250350ed5ffd3c1514ec99a080b1b92d7c8374"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6c144e08dfad4106effc043a026e5d0c0eff6ad031904c70bf5090c63f3a6a7"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:018f93de7d5318d2fb440f846839a4464738468c3476d5c9cf7da45bb71c18bd"}, - {file = "gevent-22.10.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7ed2346eb9dc4344f9cb0d7963ce5b74fe16fdd031a2809bb6c2b6eba7ebcd5"}, - {file = "gevent-22.10.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84c517e33ed604fa06b7d756dc0171169cc12f7fdd68eb7b17708a62eebf4516"}, - {file = "gevent-22.10.2-cp37-cp37m-win32.whl", hash = "sha256:4114f0f439f0b547bb6f1d474fee99ddb46736944ad2207cef3771828f6aa358"}, - {file = "gevent-22.10.2-cp37-cp37m-win_amd64.whl", hash = "sha256:0d581f22a5be6281b11ad6309b38b18f0638cf896931223cbaa5adb904826ef6"}, - {file = "gevent-22.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2929377c8ebfb6f4d868d161cd8de2ea6b9f6c7a5fcd4f78bcd537319c16190b"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:efc003b6c1481165af61f0aeac248e0a9ac8d880bb3acbe469b448674b2d5281"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db562a8519838bddad0c439a2b12246bab539dd50e299ea7ff3644274a33b6a5"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1472012493ca1fac103f700d309cb6ef7964dcdb9c788d1768266e77712f5e49"}, - {file = "gevent-22.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c04ee32c11e9fcee47c1b431834878dc987a7a2cc4fe126ddcae3bad723ce89"}, - {file = "gevent-22.10.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8729129edef2637a8084258cb9ec4e4d5ca45d97ac77aa7a6ff19ccb530ab731"}, - {file = "gevent-22.10.2-cp38-cp38-win32.whl", hash = "sha256:ae90226074a6089371a95f20288431cd4b3f6b0b096856afd862e4ac9510cddd"}, - {file = "gevent-22.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:494c7f29e94df9a1c3157d67bb7edfa32a46eed786e04d9ee68d39f375e30001"}, - {file = "gevent-22.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:58898dbabb5b11e4d0192aae165ad286dc6742c543e1be9d30dc82753547c508"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:4197d423e198265eef39a0dea286ef389da9148e070310f34455ecee8172c391"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da4183f0b9d9a1e25e1758099220d32c51cc2c6340ee0dea3fd236b2b37598e4"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5488eba6a568b4d23c072113da4fc0feb1b5f5ede7381656dc913e0d82204e2"}, - {file = "gevent-22.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:319d8b1699b7b8134de66d656cd739b308ab9c45ace14d60ae44de7775b456c9"}, - {file = "gevent-22.10.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f3329bedbba4d3146ae58c667e0f9ac1e6f1e1e6340c7593976cdc60aa7d1a47"}, - {file = "gevent-22.10.2-cp39-cp39-win32.whl", hash = "sha256:172caa66273315f283e90a315921902cb6549762bdcb0587fd60cb712a9d6263"}, - {file = "gevent-22.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:323b207b281ba0405fea042067fa1a61662e5ac0d574ede4ebbda03efd20c350"}, - {file = "gevent-22.10.2-pp27-pypy_73-win_amd64.whl", hash = "sha256:ed7f16613eebf892a6a744d7a4a8f345bc6f066a0ff3b413e2479f9c0a180193"}, - {file = "gevent-22.10.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:a47a4e77e2bc668856aad92a0b8de7ee10768258d93cd03968e6c7ba2e832f76"}, - {file = "gevent-22.10.2.tar.gz", hash = "sha256:1ca01da176ee37b3527a2702f7d40dbc9ffb8cfc7be5a03bfa4f9eec45e55c46"}, -] - -[package.dependencies] -cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} -greenlet = {version = ">=2.0.0", markers = "platform_python_implementation == \"CPython\""} -setuptools = "*" -"zope.event" = "*" -"zope.interface" = "*" - -[package.extras] -dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] -docs = ["repoze.sphinx.autointerface", "sphinxcontrib-programoutput", "zope.schema"] -monitor = ["psutil (>=5.7.0)"] -recommended = ["backports.socketpair", "cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)", "selectors2"] -test = ["backports.socketpair", "cffi (>=1.12.2)", "contextvars (==2.4)", "coverage (>=5.0)", "coveralls (>=1.7.0)", "dnspython (>=1.16.0,<2.0)", "futures", "idna", "mock", "objgraph", "psutil (>=5.7.0)", "requests", "selectors2"] - -[[package]] -name = "greenlet" -version = "3.0.3" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.7" -files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] - -[[package]] -name = "gunicorn" -version = "21.2.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.5" -files = [ - {file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"}, - {file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "har2case" -version = "0.3.1" -description = "Convert HAR(HTTP Archive) to YAML/JSON testcases for HttpRunner." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -files = [ - {file = "har2case-0.3.1-py2.py3-none-any.whl", hash = "sha256:84d3a5cc9fbb16e45372e7e880a936c59bbe8e9b66bad81927769e64f608e2af"}, - {file = "har2case-0.3.1.tar.gz", hash = "sha256:8f159ec7cba82ec4282f46af4a9dac89f65e62796521b2426d3c89c3c9fd8579"}, -] - -[package.dependencies] -PyYAML = "*" - -[[package]] -name = "idna" -version = "2.8" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, - {file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, -] - -[[package]] -name = "immutables" -version = "0.20" -description = "Immutable Collections" -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "immutables-0.20-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dea0ae4d7f31b145c18c16badeebc2f039d09411be4a8febb86e1244cf7f1ce0"}, - {file = "immutables-0.20-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2dd0dcef2f8d4523d34dbe1d2b7804b3d2a51fddbd104aad13f506a838a2ea15"}, - {file = "immutables-0.20-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393dde58ffd6b4c089ffdf4cef5fe73dad37ce4681acffade5f5d5935ec23c93"}, - {file = "immutables-0.20-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1214b5a175df783662b7de94b4a82db55cc0ee206dd072fa9e279fb8895d8df"}, - {file = "immutables-0.20-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2761e3dc2a6406943ce77b3505e9b3c1187846de65d7247548dc7edaa202fcba"}, - {file = "immutables-0.20-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2bcea81e7516bd823b4ed16f4f794531097888675be13e833b1cc946370d5237"}, - {file = "immutables-0.20-cp310-cp310-win32.whl", hash = "sha256:d828e7580f1fa203ddeab0b5e91f44bf95706e7f283ca9fbbcf0ae08f63d3084"}, - {file = "immutables-0.20-cp310-cp310-win_amd64.whl", hash = "sha256:380e2957ba3d63422b2f3fbbff0547c7bbe6479d611d3635c6411005a4264525"}, - {file = "immutables-0.20-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:532be32c7a25dae6cade28825c76d3004cf4d166a0bfacf04bda16056d59ba26"}, - {file = "immutables-0.20-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5302ce9c7827f8300f3dc34a695abb71e4a32bab09e65e5ad6e454785383347f"}, - {file = "immutables-0.20-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b51aec54b571ae466113509d4dc79a2808dc2ae9263b71fd6b37778cb49eb292"}, - {file = "immutables-0.20-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f56aea56e597ecf6631f24a4e26007b6a5f4fe30278b96eb90bc1f60506164"}, - {file = "immutables-0.20-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:085ac48ee3eef7baf070f181cae574489bbf65930a83ec5bbd65c9940d625db3"}, - {file = "immutables-0.20-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f063f53b5c0e8f541ae381f1d828f3d05bbed766a2d6c817f9218b8b37a4cb66"}, - {file = "immutables-0.20-cp311-cp311-win32.whl", hash = "sha256:b0436cc831b47e26bef637bcf143cf0273e49946cfb7c28c44486d70513a3080"}, - {file = "immutables-0.20-cp311-cp311-win_amd64.whl", hash = "sha256:5bb32aee1ea16fbb90f58f8bd96016bca87aba0a8e574e5fa218d0d83b142851"}, - {file = "immutables-0.20-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ba726b7a3a696b9d4b122fa2c956bc68e866f3df1b92765060c88c64410ff82"}, - {file = "immutables-0.20-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5a88adf1dcc9d8ab07dba5e74deefcd5b5e38bc677815cbf9365dc43b69f1f08"}, - {file = "immutables-0.20-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1009a4e00e2e69a9b40c2f1272795f5a06ad72c9bf4638594d518e9cbd7a721a"}, - {file = "immutables-0.20-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96899994842c37cf4b9d6d2bedf685aae7810bd73f1538f8cba5426e2d65cb85"}, - {file = "immutables-0.20-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a606410b2ccb6ae339c3f26cccc9a92bcb16dc06f935d51edfd8ca68cf687e50"}, - {file = "immutables-0.20-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8e82754f72823085643a2c0e6a4c489b806613e94af205825fa81df2ba147a0"}, - {file = "immutables-0.20-cp312-cp312-win32.whl", hash = "sha256:525fb361bd7edc8a891633928d549713af8090c79c25af5cc06eb90b48cb3c64"}, - {file = "immutables-0.20-cp312-cp312-win_amd64.whl", hash = "sha256:a82afc3945e9ceb9bcd416dc4ed9b72f92760c42787e26de50610a8b81d48120"}, - {file = "immutables-0.20-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f17f25f21e82a1c349a61191cfb13e442a348b880b74cb01b00e0d1e848b63f4"}, - {file = "immutables-0.20-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:65954eb861c61af48debb1507518d45ae7d594b4fba7282785a70b48c5f51f9b"}, - {file = "immutables-0.20-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62f8a7a22939278127b7a206d05679b268b9cf665437125625348e902617cbad"}, - {file = "immutables-0.20-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac86f4372f4cfaa00206c12472fd3a78753092279e0552b7e1880944d71b04fe"}, - {file = "immutables-0.20-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e771198edc11a9e02ffa693911b3918c6cde0b64ad2e6672b076dbe005557ad8"}, - {file = "immutables-0.20-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fc739fc07cff5df2e4f31addbd48660b5ac0da56e9f719f8bb45da8ddd632c63"}, - {file = "immutables-0.20-cp38-cp38-win32.whl", hash = "sha256:c086ccb44d9d3824b9bf816365d10b1b82837efc7119f8bab56bd7a27ed805a9"}, - {file = "immutables-0.20-cp38-cp38-win_amd64.whl", hash = "sha256:9cd2ee9c10bf00be3c94eb51854bc0b761326bd0a7ea0dad4272a3f182269ae6"}, - {file = "immutables-0.20-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d4f78cb748261f852953620ed991de74972446fd484ec69377a41e2f1a1beb75"}, - {file = "immutables-0.20-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6449186ea91b7c17ec8e7bd9bf059858298b1db5c053f5d27de8eba077578ce"}, - {file = "immutables-0.20-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85dd9765b068f7beb297553fddfcf7f904bd58a184c520830a106a58f0c9bfb4"}, - {file = "immutables-0.20-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f349a7e0327b92dcefb863e49ace086f2f26e6689a4e022c98720c6e9696e763"}, - {file = "immutables-0.20-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e3a5462f6d3549bbf7d02ce929fb0cb6df9539445f0589105de4e8b99b906e69"}, - {file = "immutables-0.20-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cc51a01a64a6d2cd7db210a49ad010c2ac2e9e026745f23fd31e0784096dcfff"}, - {file = "immutables-0.20-cp39-cp39-win32.whl", hash = "sha256:83794712f0507416f2818edc63f84305358b8656a93e5b9e2ab056d9803c7507"}, - {file = "immutables-0.20-cp39-cp39-win_amd64.whl", hash = "sha256:2837b1078abc66d9f009bee9085cf62515d5516af9a5c9ea2751847e16efd236"}, - {file = "immutables-0.20.tar.gz", hash = "sha256:1d2f83e6a6a8455466cd97b9a90e2b4f7864648616dfa6b19d18f49badac3876"}, -] - -[package.extras] -test = ["flake8 (>=5.0,<6.0)", "mypy (>=1.4,<2.0)", "pycodestyle (>=2.9,<3.0)", "pytest (>=7.4,<8.0)"] - -[[package]] -name = "inflection" -version = "0.5.0" -description = "A port of Ruby on Rails inflector to Python" -optional = false -python-versions = ">=3.5" -files = [ - {file = "inflection-0.5.0-py2.py3-none-any.whl", hash = "sha256:88b101b2668a1d81d6d72d4c2018e53bc6c7fc544c987849da1c7f77545c3bc9"}, - {file = "inflection-0.5.0.tar.gz", hash = "sha256:f576e85132d34f5bf7df5183c2c6f94cfb32e528f53065345cf71329ba0b8924"}, -] - -[[package]] -name = "itypes" -version = "1.2.0" -description = "Simple immutable types for python." -optional = false -python-versions = "*" -files = [ - {file = "itypes-1.2.0-py2.py3-none-any.whl", hash = "sha256:03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6"}, - {file = "itypes-1.2.0.tar.gz", hash = "sha256:af886f129dea4a2a1e3d36595a2d139589e4dd287f5cab0b40e799ee81570ff1"}, -] - -[[package]] -name = "jinja2" -version = "2.10.3" -description = "A very fast and expressive template engine." -optional = false -python-versions = "*" -files = [ - {file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, - {file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, -] - -[package.dependencies] -MarkupSafe = ">=0.23" - -[package.extras] -i18n = ["Babel (>=0.8)"] - -[[package]] -name = "json5" -version = "0.9.5" -description = "A Python implementation of the JSON5 data format." -optional = false -python-versions = "*" -files = [ - {file = "json5-0.9.5-py2.py3-none-any.whl", hash = "sha256:af1a1b9a2850c7f62c23fde18be4749b3599fd302f494eebf957e2ada6b9e42c"}, - {file = "json5-0.9.5.tar.gz", hash = "sha256:703cfee540790576b56a92e1c6aaa6c4b0d98971dc358ead83812aa4d06bdb96"}, -] - -[package.extras] -dev = ["hypothesis"] - -[[package]] -name = "jsonpath" -version = "0.82.2" -description = "An XPath for JSON" -optional = false -python-versions = "*" -files = [ - {file = "jsonpath-0.82.2.tar.gz", hash = "sha256:d87ef2bcbcded68ee96bc34c1809b69457ecec9b0c4dd471658a12bd391002d1"}, -] - -[[package]] -name = "kombu" -version = "5.3.5" -description = "Messaging library for Python." -optional = false -python-versions = ">=3.8" -files = [ - {file = "kombu-5.3.5-py3-none-any.whl", hash = "sha256:0eac1bbb464afe6fb0924b21bf79460416d25d8abc52546d4f16cad94f789488"}, - {file = "kombu-5.3.5.tar.gz", hash = "sha256:30e470f1a6b49c70dc6f6d13c3e4cc4e178aa6c469ceb6bcd55645385fc84b93"}, -] - -[package.dependencies] -amqp = ">=5.1.1,<6.0.0" -vine = "*" - -[package.extras] -azureservicebus = ["azure-servicebus (>=7.10.0)"] -azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] -confluentkafka = ["confluent-kafka (>=2.2.0)"] -consul = ["python-consul2"] -librabbitmq = ["librabbitmq (>=2.0.0)"] -mongodb = ["pymongo (>=4.1.1)"] -msgpack = ["msgpack"] -pyro = ["pyro4"] -qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] -redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] -slmq = ["softlayer-messaging (>=1.0.3)"] -sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] -sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] -yaml = ["PyYAML (>=3.10)"] -zookeeper = ["kazoo (>=2.8.0)"] - -[[package]] -name = "loguru" -version = "0.5.1" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = ">=3.5" -files = [ - {file = "loguru-0.5.1-py3-none-any.whl", hash = "sha256:e5d362a43cd2fc2da63551d79a6830619c4d5b3a8b976515748026f92f351b61"}, - {file = "loguru-0.5.1.tar.gz", hash = "sha256:70201d5fce26da89b7a5f168caa2bb674e06b969829f56737db1d6472e53e7c3"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (>=2.2.1)", "black (>=19.3b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=4.3.20)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] - -[[package]] -name = "markupsafe" -version = "1.1.1" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -files = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, -] - -[[package]] -name = "openapi-codec" -version = "1.3.2" -description = "An OpenAPI codec for Core API." -optional = false -python-versions = "*" -files = [ - {file = "openapi-codec-1.3.2.tar.gz", hash = "sha256:1bce63289edf53c601ea3683120641407ff6b708803b8954c8a876fe778d2145"}, -] - -[package.dependencies] -coreapi = ">=2.2.0" - -[[package]] -name = "packaging" -version = "23.2" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.7" -files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.43" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, - {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "pyasn1" -version = "0.5.1" -description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pyasn1-0.5.1-py2.py3-none-any.whl", hash = "sha256:4439847c58d40b1d0a573d07e3856e95333f1976294494c325775aeca506eb58"}, - {file = "pyasn1-0.5.1.tar.gz", hash = "sha256:6d391a96e59b23130a5cfa74d6fd7f388dbbe26cc8f1edf39fdddf08d9d6676c"}, -] - -[[package]] -name = "pyasn1-modules" -version = "0.3.0" -description = "A collection of ASN.1-based protocols modules" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, -] - -[package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" - -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - -[[package]] -name = "pydantic" -version = "1.9.0" -description = "Data validation and settings management using python 3.6 type hinting" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, - {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, - {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, - {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, - {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, - {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, - {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, - {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, - {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, - {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, - {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, -] - -[package.dependencies] -typing-extensions = ">=3.7.4.3" - -[package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] - -[[package]] -name = "pydash" -version = "5.0.2" -description = "The kitchen sink of Python utility libraries for doing \"stuff\" in a functional way. Based on the Lo-Dash Javascript library." -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydash-5.0.2-py3-none-any.whl", hash = "sha256:a0dfc36087b491653c7fbff4a04a52e1b58b67d3aa751d15e0dbb96fb7e09833"}, - {file = "pydash-5.0.2.tar.gz", hash = "sha256:7c02f5c27524abbd90743c4b60fd8c8c8e846ee0439642704f77a3cf21f7c371"}, -] - -[package.extras] -dev = ["Sphinx", "black", "coverage", "docformatter", "flake8", "flake8-black", "flake8-bugbear", "flake8-isort", "invoke", "isort", "pylint", "pytest", "pytest-cov", "pytest-flake8", "pytest-pylint", "sphinx-rtd-theme", "tox", "twine", "wheel"] - -[[package]] -name = "pyjwt" -version = "1.7.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = "*" -files = [ - {file = "PyJWT-1.7.1-py2.py3-none-any.whl", hash = "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e"}, - {file = "PyJWT-1.7.1.tar.gz", hash = "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96"}, -] - -[package.extras] -crypto = ["cryptography (>=1.4)"] -flake8 = ["flake8", "flake8-import-order", "pep8-naming"] -test = ["pytest (>=4.0.1,<5.0.0)", "pytest-cov (>=2.6.0,<3.0.0)", "pytest-runner (>=4.2,<5.0.0)"] - -[[package]] -name = "pymysql" -version = "1.1.0" -description = "Pure Python MySQL Driver" -optional = false -python-versions = ">=3.7" -files = [ - {file = "PyMySQL-1.1.0-py3-none-any.whl", hash = "sha256:8969ec6d763c856f7073c4c64662882675702efcb114b4bcbb955aea3a069fa7"}, - {file = "PyMySQL-1.1.0.tar.gz", hash = "sha256:4f13a7df8bf36a51e81dd9f3605fede45a4878fe02f9236349fd82a3f0612f96"}, -] - -[package.extras] -ed25519 = ["PyNaCl (>=1.4.0)"] -rsa = ["cryptography"] - -[[package]] -name = "pyparsing" -version = "2.4.7" -description = "Python parsing module" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] - -[[package]] -name = "python-crontab" -version = "3.0.0" -description = "Python Crontab API" -optional = false -python-versions = "*" -files = [ - {file = "python-crontab-3.0.0.tar.gz", hash = "sha256:79fb7465039ddfd4fb93d072d6ee0d45c1ac8bf1597f0686ea14fd4361dba379"}, - {file = "python_crontab-3.0.0-py3-none-any.whl", hash = "sha256:6d5ba3c190ec76e4d252989a1644fcb233dbf53fbc8fceeb9febe1657b9fb1d4"}, -] - -[package.dependencies] -python-dateutil = "*" - -[package.extras] -cron-description = ["cron-descriptor"] -cron-schedule = ["croniter"] - -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "0.10.3" -description = "Add .env support to your django/flask apps in development and deployments" -optional = false -python-versions = "*" -files = [ - {file = "python-dotenv-0.10.3.tar.gz", hash = "sha256:f157d71d5fec9d4bd5f51c82746b6344dffa680ee85217c123f4a0c8117c4544"}, - {file = "python_dotenv-0.10.3-py2.py3-none-any.whl", hash = "sha256:debd928b49dbc2bf68040566f55cdb3252458036464806f4094487244e2a4093"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "python-ldap" -version = "3.4.4" -description = "Python modules for implementing LDAP clients" -optional = false -python-versions = ">=3.6" -files = [ - {file = "python-ldap-3.4.4.tar.gz", hash = "sha256:7edb0accec4e037797705f3a05cbf36a9fde50d08c8f67f2aef99a2628fab828"}, -] - -[package.dependencies] -pyasn1 = ">=0.3.7" -pyasn1_modules = ">=0.1.5" - -[[package]] -name = "pytz" -version = "2022.2.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, - {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, -] - -[[package]] -name = "pyyaml" -version = "5.3.1" -description = "YAML parser and emitter for Python" -optional = false -python-versions = "*" -files = [ - {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, - {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, - {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, - {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, - {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, - {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, - {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, - {file = "PyYAML-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a"}, - {file = "PyYAML-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e"}, - {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, -] - -[[package]] -name = "requests" -version = "2.22.0" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -files = [ - {file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, - {file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<3.1.0" -idna = ">=2.5,<2.9" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" - -[package.extras] -security = ["cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)"] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] - -[[package]] -name = "requests-toolbelt" -version = "0.9.1" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = "*" -files = [ - {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, - {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "ruff" -version = "0.2.2" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, -] - -[[package]] -name = "sentry-sdk" -version = "1.5.12" -description = "Python client for Sentry (https://sentry.io)" -optional = false -python-versions = "*" -files = [ - {file = "sentry-sdk-1.5.12.tar.gz", hash = "sha256:259535ba66933eacf85ab46524188c84dcb4c39f40348455ce15e2c0aca68863"}, - {file = "sentry_sdk-1.5.12-py2.py3-none-any.whl", hash = "sha256:778b53f0a6c83b1ee43d3b7886318ba86d975e686cb2c7906ccc35b334360be1"}, -] - -[package.dependencies] -certifi = "*" -urllib3 = ">=1.10.0" - -[package.extras] -aiohttp = ["aiohttp (>=3.5)"] -beam = ["apache-beam (>=2.12)"] -bottle = ["bottle (>=0.12.13)"] -celery = ["celery (>=3)"] -chalice = ["chalice (>=1.16.0)"] -django = ["django (>=1.8)"] -falcon = ["falcon (>=1.4)"] -flask = ["blinker (>=1.1)", "flask (>=0.11)"] -httpx = ["httpx (>=0.16.0)"] -pure-eval = ["asttokens", "executing", "pure-eval"] -pyspark = ["pyspark (>=2.4.4)"] -quart = ["blinker (>=1.1)", "quart (>=0.16.1)"] -rq = ["rq (>=0.6)"] -sanic = ["sanic (>=0.8)"] -sqlalchemy = ["sqlalchemy (>=1.2)"] -tornado = ["tornado (>=5)"] - -[[package]] -name = "setuptools" -version = "69.1.1" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, -] - -[package.extras] -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)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "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.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "simplejson" -version = "3.17.0" -description = "Simple, fast, extensible JSON encoder/decoder for Python" -optional = false -python-versions = "*" -files = [ - {file = "simplejson-3.17.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17"}, - {file = "simplejson-3.17.0-cp27-cp27m-win32.whl", hash = "sha256:1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6"}, - {file = "simplejson-3.17.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb"}, - {file = "simplejson-3.17.0-cp33-cp33m-win32.whl", hash = "sha256:8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1"}, - {file = "simplejson-3.17.0-cp33-cp33m-win_amd64.whl", hash = "sha256:22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866"}, - {file = "simplejson-3.17.0-cp34-cp34m-win32.whl", hash = "sha256:17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882"}, - {file = "simplejson-3.17.0-cp34-cp34m-win_amd64.whl", hash = "sha256:0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2"}, - {file = "simplejson-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8"}, - {file = "simplejson-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"}, - {file = "simplejson-3.17.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614"}, - {file = "simplejson-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5"}, - {file = "simplejson-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a"}, - {file = "simplejson-3.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3"}, - {file = "simplejson-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159"}, - {file = "simplejson-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc"}, - {file = "simplejson-3.17.0.tar.gz", hash = "sha256:2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81"}, -] - -[[package]] -name = "six" -version = "1.15.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] - -[[package]] -name = "sqlparse" -version = "0.3.1" -description = "Non-validating SQL parser" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "sqlparse-0.3.1-py2.py3-none-any.whl", hash = "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e"}, - {file = "sqlparse-0.3.1.tar.gz", hash = "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"}, -] - -[[package]] -name = "text-unidecode" -version = "1.3" -description = "The most basic Text::Unidecode port" -optional = false -python-versions = "*" -files = [ - {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, - {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, -] - -[[package]] -name = "tornado" -version = "6.4" -description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." -optional = false -python-versions = ">= 3.8" -files = [ - {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, - {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, - {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, - {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, - {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, - {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, - {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, -] - -[[package]] -name = "typing-extensions" -version = "4.9.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, -] - -[[package]] -name = "tzdata" -version = "2024.1" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "uritemplate" -version = "3.0.1" -description = "URI templates" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "uritemplate-3.0.1-py2.py3-none-any.whl", hash = "sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f"}, - {file = "uritemplate-3.0.1.tar.gz", hash = "sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"}, -] - -[[package]] -name = "urllib3" -version = "1.25.7" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" -files = [ - {file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, - {file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, -] - -[package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "vine" -version = "5.1.0" -description = "Python promises." -optional = false -python-versions = ">=3.6" -files = [ - {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, - {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, -] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -files = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, -] - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - -[[package]] -name = "xmltodict" -version = "0.12.0" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, - {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, -] - -[[package]] -name = "zope-event" -version = "5.0" -description = "Very basic event publishing system" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, - {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -docs = ["Sphinx"] -test = ["zope.testrunner"] - -[[package]] -name = "zope-interface" -version = "6.2" -description = "Interfaces for Python" -optional = false -python-versions = ">=3.7" -files = [ - {file = "zope.interface-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab"}, - {file = "zope.interface-6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f"}, - {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328"}, - {file = "zope.interface-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037"}, - {file = "zope.interface-6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad"}, - {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac"}, - {file = "zope.interface-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48"}, - {file = "zope.interface-6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000"}, - {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b"}, - {file = "zope.interface-6.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe"}, - {file = "zope.interface-6.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000"}, - {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b"}, - {file = "zope.interface-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532"}, - {file = "zope.interface-6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd"}, - {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3"}, - {file = "zope.interface-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099"}, - {file = "zope.interface-6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f"}, - {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a"}, - {file = "zope.interface-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1"}, - {file = "zope.interface-6.2.tar.gz", hash = "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565"}, -] - -[package.dependencies] -setuptools = "*" - -[package.extras] -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"] - -[metadata] -lock-version = "2.0" -python-versions = "^3.11" -content-hash = "b95da891a45054699a19fae84a2706937380cd6e2003a3b39fa8401aca41d914" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index bcc5c8cf..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,104 +0,0 @@ -[tool.poetry] -name = "fasterrunner" -version = "3.0.0" -description = "" -authors = ["lihuacai "] -readme = "README.md" - -[tool.poetry.dependencies] -python = "^3.11" -aiocontextvars = "0.2.2" -amqp = "^5.2.0" -beautifulsoup4 = "4.6.3" -celery = ">=5.2.7,<5.3.0" -certifi = "2019.9.11" -chardet = "3.0.4" -colorama = "0.4.1" -colorlog = "4.0.2" -contextvars = "2.4" -coreapi = "2.3.3" -coreschema = "0.0.4" -dingtalkchatbot = "1.3.0" -django = "4.1.13" -django-celery-beat = "^2.5.0" -django-cors-headers = "4.3.1" -django-jsonfield = "1.4.0" -django-model-utils = "4.0.0" -django-rest-swagger = "2.2.0" -django-simpleui = "2023.12.12" -django-utils-six = "2.0" -djangorestframework = "3.14.0" -djangorestframework-jwt = "1.11.0" -drf-yasg = "1.21.7" -packaging = "23.2" -har2case = "0.3.1" -idna = "2.8" -inflection = "0.5.0" -jinja2 = "2.10.3" -loguru = "0.5.1" -markupsafe = "1.1.1" -openapi-codec = "1.3.2" -pyjwt = "1.7.1" -pyparsing = "2.4.7" -python-dotenv = "0.10.3" -pytz = "2022.2.1" -pyyaml = ">=5.3.1,<5.4.0" -requests = "2.22.0" -requests-toolbelt = "0.9.1" -simplejson = "3.17.0" -six = "1.15.0" -sqlparse = "0.3.1" -tornado = "6.4" -uritemplate = "3.0.1" -urllib3 = "1.25.7" -django-mysql = "^4.12.0" -json5 = "0.9.5" -django-bulk-update = "2.2.0" -xmltodict = ">=0.12.0,<0.13.0" -genson = ">=1.2.2,<1.3.0" -faker = ">=7.0.1,<7.1.0" -curlify = ">=2.2.1,<2.3.0" -pydash = ">=5.0.1,<5.1.0" -jsonpath = ">=0.82,<1.0" -pydantic = "1.9.0" -gunicorn = "^21.2.0" -sentry-sdk = ">=1.5.8,<1.6.0" -croniter = ">=1.3.5,<1.4.0" -django-log-request-id = ">=2.0.0,<2.1.0" -gevent = "22.10.2" -django-filter = ">=2.4.0,<2.5.0" -django-auth-ldap = "2.3.0" -pymysql = "1.1.0" - - -[tool.poetry.group.dev.dependencies] -ruff = "^0.2.2" - -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" - - -[project] -# Support Python 3.11+. -requires-python = ">=3.11" - -[tool.ruff] -# Set the maximum line length to 79. -line-length = 120 -exclude = ["web/*", "**/migrations/**"] - -[tool.ruff.lint] -# Add the `line-too-long` rule to the enforced rule set. By default, Ruff omits rules that -# overlap with the use of a formatter, like Black, but we can override this behavior by -# explicitly adding the rule. -extend-select = ["E501"] -select = [ - # isort - "I", - # pyupgrade - # "UP", -] - -[tool.ruff.lint.pydocstyle] -convention = "google" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c0749b51 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,70 @@ +# setuptools~=57.5.0 +aiocontextvars==0.2.2 +amqp +# anyjson==0.3.3 +beautifulsoup4==4.6.3 +# billiard==3.3.0.23 +celery~=5.2.7 +certifi==2019.9.11 +chardet==3.0.4 +colorama==0.4.1 +colorlog==4.0.2 +contextvars==2.4 +coreapi==2.3.3 +coreschema==0.0.4 +DingtalkChatbot==1.3.0 +Django==4.1.13 +django-celery-beat +django-cors-headers==4.3.1 +django-jsonfield==1.4.0 +django-model-utils==4.0.0 +django-rest-swagger==2.2.0 +django-simpleui==2023.12.12 +django-utils-six==2.0 +djangorestframework==3.14.0 +djangorestframework-jwt==1.11.0 +drf-yasg==1.21.7 +packaging==23.2 +har2case==0.3.1 +# HttpRunner==1.5.15 +idna==2.8 +inflection==0.5.0 +Jinja2==2.10.3 +# kombu +loguru==0.5.1 +MarkupSafe==1.1.1 +#mysqlclient==2.0.1 +openapi-codec==1.3.2 +PyJWT==1.7.1 +pyparsing==2.4.7 +python-dotenv==0.10.3 +pytz==2022.2.1 +PyYAML~=5.3.1 +requests==2.22.0 +requests-toolbelt==0.9.1 +simplejson==3.17.0 +six==1.15.0 +sqlparse==0.3.1 +tornado==6.4 +uritemplate==3.0.1 +urllib3==1.25.7 +#uWSGI==2.0.18 +django_mysql +json5==0.9.5 +django_bulk_update==2.2.0 +xmltodict~=0.12.0 +django-jsonfield==1.4.0 +genson~=1.2.2 +Faker~=7.0.1 +curlify~=2.2.1 +pydash~=5.0.1 +jsonpath~=0.82 +pydantic==1.9.0 +gunicorn +sentry-sdk~=1.5.8 +croniter~=1.3.5 # 计算下一次crontab +django-log-request-id~=2.0.0 +gevent==22.10.2 +django-filter~=2.4.0 +django-auth-ldap==2.3.0 +pymysql==1.1.0 diff --git a/system/admin.py b/system/admin.py index 846f6b40..8c38f3f3 100644 --- a/system/admin.py +++ b/system/admin.py @@ -1 +1,3 @@ +from django.contrib import admin + # Register your models here. diff --git a/system/apps.py b/system/apps.py index 1806cc44..5dc4d64b 100644 --- a/system/apps.py +++ b/system/apps.py @@ -2,4 +2,4 @@ class SystemConfig(AppConfig): - name = "system" + name = 'system' diff --git a/system/models.py b/system/models.py index b93dbf08..0fa10253 100644 --- a/system/models.py +++ b/system/models.py @@ -2,13 +2,14 @@ from fastuser.models import BaseTable + # Create your models here. class LogRecord(BaseTable): class Meta: db_table = "log_record" - request_id = models.CharField(max_length=100, null=True, db_index=True) level = models.CharField(max_length=20) message = models.TextField(db_index=True) + diff --git a/system/serializers/__init__.py b/system/serializers/__init__.py index 47ea6863..b2a702a2 100644 --- a/system/serializers/__init__.py +++ b/system/serializers/__init__.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: __init__.py.py diff --git a/system/serializers/log_record_serializer.py b/system/serializers/log_record_serializer.py index c3097837..56d2fa1e 100644 --- a/system/serializers/log_record_serializer.py +++ b/system/serializers/log_record_serializer.py @@ -1,4 +1,5 @@ # !/usr/bin/python3 +# -*- coding: utf-8 -*- # @Author: 花菜 # @File: log_record_serializer.py @@ -6,11 +7,9 @@ # @Email: lihuacai168@gmail.com from rest_framework import serializers - from system.models import LogRecord - class LogRecordSerializer(serializers.ModelSerializer): class Meta: model = LogRecord - fields = "__all__" + fields = '__all__' diff --git a/system/tests.py b/system/tests.py index a39b155a..7ce503c2 100644 --- a/system/tests.py +++ b/system/tests.py @@ -1 +1,3 @@ +from django.test import TestCase + # Create your tests here. diff --git a/system/views.py b/system/views.py index f59919be..271aee8a 100644 --- a/system/views.py +++ b/system/views.py @@ -1,31 +1,25 @@ +from rest_framework import viewsets, filters, status from django_filters import rest_framework as django_filters -from rest_framework import filters, status, viewsets from rest_framework.response import Response -from system.serializers.log_record_serializer import LogRecordSerializer - from .models import LogRecord - +from system.serializers.log_record_serializer import LogRecordSerializer class LogRecordFilter(django_filters.FilterSet): - request_id = django_filters.CharFilter(field_name="request_id", lookup_expr="exact") - message = django_filters.CharFilter(field_name="message", lookup_expr="icontains") + request_id = django_filters.CharFilter(field_name='request_id', lookup_expr='exact') + message = django_filters.CharFilter(field_name='message', lookup_expr='icontains') class Meta: model = LogRecord - fields = ["request_id", "message"] - + fields = ['request_id', 'message'] class LogRecordViewSet(viewsets.ModelViewSet): queryset = LogRecord.objects.all() serializer_class = LogRecordSerializer - filter_backends = ( - django_filters.DjangoFilterBackend, - filters.OrderingFilter, - ) + filter_backends = (django_filters.DjangoFilterBackend, filters.OrderingFilter) filterset_class = LogRecordFilter - ordering_fields = ["create_time"] - ordering = ["create_time"] + ordering_fields = ['create_time'] + ordering = ['create_time'] def destroy(self, request, *args, **kwargs): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) @@ -34,4 +28,4 @@ def update(self, request, *args, **kwargs): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) def partial_update(self, request, *args, **kwargs): - return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) + return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) \ No newline at end of file