- 统一标注引擎为 CVAT:客户端/配置/格式转换、iframe 标注页、docker-compose.cvat.yml 与 no_auth 补丁 - 移除 Label Studio 相关配置与构建脚本,清理 embedded.bak 备份与误提交的 node_modules - 新增「我的标注」:跨 Campaign 收件箱、逐张清单、CVAT frame 跳转 - 飞书任务分配:通讯录同步选人、按量分配、分配后 DM 通知(含 my-tasks 链接) - ADAS cuboid_7cls 数据湖接入:workflow 路径、register-batch、开标上传与标注同步 - 数据湖挂载 AS_DATA_LAKE_ROOT、datasets/adas 符号链接、reset_labeling 运维脚本 - 补充 docs/HANDOVER.md 项目交接文档 Co-authored-by: Cursor <cursoragent@cursor.com>
825 lines
28 KiB
Python
825 lines
28 KiB
Python
# Copyright (C) 2018-2022 Intel Corporation
|
||
# Copyright (C) CVAT.ai Corporation
|
||
#
|
||
# SPDX-License-Identifier: MIT
|
||
|
||
"""
|
||
Django settings for CVAT project.
|
||
|
||
Generated by 'django-admin startproject' using Django 2.0.1.
|
||
|
||
For more information on this file, see
|
||
https://docs.djangoproject.com/en/2.0/topics/settings/
|
||
|
||
For the full list of settings and their values, see
|
||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
import urllib
|
||
from datetime import timedelta
|
||
from enum import Enum, IntEnum
|
||
from pathlib import Path
|
||
|
||
from attr.converters import to_bool
|
||
from corsheaders.defaults import default_headers
|
||
from django.core.exceptions import ImproperlyConfigured
|
||
from logstash_async.constants import constants as logstash_async_constants
|
||
|
||
from cvat import __version__
|
||
from cvat.apps.iam.password_validation import DEFAULT_MIN_PASSWORD_LENGTH
|
||
|
||
# Build paths inside the project like this: BASE_DIR / ...
|
||
BASE_DIR = Path(__file__).parents[2]
|
||
|
||
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",")
|
||
INTERNAL_IPS = ["127.0.0.1"]
|
||
SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "")
|
||
|
||
|
||
def generate_secret_key():
|
||
"""
|
||
Creates secret_key.py in such a way that multiple processes calling
|
||
this will all end up with the same key (assuming that they share the
|
||
same "keys" directory).
|
||
"""
|
||
|
||
from django.utils.crypto import get_random_string
|
||
|
||
keys_dir = BASE_DIR / "keys"
|
||
keys_dir.mkdir(exist_ok=True)
|
||
|
||
secret_key_fname = "secret_key.py" # nosec
|
||
|
||
with tempfile.NamedTemporaryFile(mode="wt", dir=keys_dir, prefix=secret_key_fname + ".") as f:
|
||
chars = "abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)"
|
||
f.write("SECRET_KEY = '{}'\n".format(get_random_string(50, chars)))
|
||
|
||
# Make sure the file contents are written before we link to it
|
||
# from the final location.
|
||
f.flush()
|
||
|
||
try:
|
||
os.link(f.name, keys_dir / secret_key_fname)
|
||
except FileExistsError:
|
||
# Somebody else created the secret key first.
|
||
# Discard ours and use theirs.
|
||
pass
|
||
|
||
|
||
if not SECRET_KEY:
|
||
sys.path.append(os.fspath(BASE_DIR))
|
||
|
||
try:
|
||
from keys.secret_key import SECRET_KEY # pylint: disable=unused-import
|
||
except ModuleNotFoundError:
|
||
generate_secret_key()
|
||
from keys.secret_key import SECRET_KEY
|
||
|
||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||
INSTALLED_APPS = [
|
||
"django.contrib.admin",
|
||
"django.contrib.auth",
|
||
"django.contrib.contenttypes",
|
||
"django.contrib.sessions",
|
||
"django.contrib.messages",
|
||
"django.contrib.staticfiles",
|
||
"django_rq",
|
||
"django_sendfile",
|
||
"dj_rest_auth",
|
||
"dj_rest_auth.registration",
|
||
"dj_pagination",
|
||
"django_filters",
|
||
"rest_framework",
|
||
"rest_framework.authtoken",
|
||
"rest_framework_api_key",
|
||
"drf_spectacular",
|
||
"django.contrib.sites",
|
||
"allauth",
|
||
"allauth.account",
|
||
"corsheaders",
|
||
"allauth.socialaccount",
|
||
"health_check",
|
||
"health_check.cache",
|
||
"health_check.db",
|
||
"health_check.contrib.psutil",
|
||
"cvat.apps.iam",
|
||
"cvat.apps.dataset_manager",
|
||
"cvat.apps.organizations",
|
||
"cvat.apps.engine",
|
||
"cvat.apps.dataset_repo",
|
||
"cvat.apps.lambda_manager",
|
||
"cvat.apps.webhooks",
|
||
"cvat.apps.health",
|
||
"cvat.apps.events",
|
||
"cvat.apps.quality_control",
|
||
"cvat.apps.redis_handler",
|
||
"cvat.apps.consensus",
|
||
"cvat.apps.access_tokens",
|
||
]
|
||
|
||
SITE_ID = 1
|
||
|
||
|
||
def parse_num_proxies(value: str | None) -> int | None:
|
||
if value in (None, ""):
|
||
return None
|
||
|
||
try:
|
||
num_proxies = int(value)
|
||
except (TypeError, ValueError):
|
||
raise ImproperlyConfigured("CVAT_NUM_PROXIES must be an integer")
|
||
|
||
if num_proxies < 0:
|
||
raise ImproperlyConfigured("CVAT_NUM_PROXIES must be a non-negative integer")
|
||
|
||
return num_proxies
|
||
|
||
|
||
REST_FRAMEWORK = {
|
||
"DEFAULT_PARSER_CLASSES": [
|
||
"rest_framework.parsers.JSONParser",
|
||
],
|
||
"DEFAULT_RENDERER_CLASSES": [
|
||
"cvat.apps.engine.renderers.CVATAPIRenderer",
|
||
"rest_framework.renderers.BrowsableAPIRenderer",
|
||
],
|
||
"DEFAULT_PERMISSION_CLASSES": [
|
||
"rest_framework.permissions.AllowAny",
|
||
],
|
||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||
"cvat.apps.iam.no_auth.NoAuthAuthentication",
|
||
],
|
||
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.AcceptHeaderVersioning",
|
||
"ALLOWED_VERSIONS": ("2.0"),
|
||
"DEFAULT_VERSION": "2.0",
|
||
"VERSION_PARAM": "version",
|
||
"DEFAULT_PAGINATION_CLASS": "cvat.apps.engine.pagination.CustomPagination",
|
||
"PAGE_SIZE": 10,
|
||
"DEFAULT_FILTER_BACKENDS": (
|
||
"cvat.apps.engine.filters.SimpleFilter",
|
||
"cvat.apps.engine.filters.SearchFilter",
|
||
"cvat.apps.engine.filters.OrderingFilter",
|
||
"cvat.apps.engine.filters.JsonLogicFilter",
|
||
"cvat.apps.iam.filters.OrganizationFilterBackend",
|
||
),
|
||
"SEARCH_PARAM": "search",
|
||
# Disable default handling of the 'format' query parameter by REST framework
|
||
"URL_FORMAT_OVERRIDE": "scheme",
|
||
"DEFAULT_THROTTLE_CLASSES": [
|
||
"rest_framework.throttling.AnonRateThrottle",
|
||
"rest_framework.throttling.ScopedRateThrottle",
|
||
],
|
||
"DEFAULT_THROTTLE_RATES": {
|
||
"anon": "100/minute",
|
||
# dj-rest-auth views define this scope. Keep them unthrottled by default.
|
||
"dj_rest_auth": None,
|
||
},
|
||
"NUM_PROXIES": parse_num_proxies(os.getenv("CVAT_NUM_PROXIES", "0")),
|
||
"DEFAULT_METADATA_CLASS": "rest_framework.metadata.SimpleMetadata",
|
||
"DEFAULT_SCHEMA_CLASS": "cvat.apps.iam.schema.CustomAutoSchema",
|
||
"EXCEPTION_HANDLER": "cvat.apps.events.handlers.handle_viewset_exception",
|
||
}
|
||
|
||
|
||
REST_AUTH = {
|
||
"REGISTER_SERIALIZER": "cvat.apps.iam.serializers.RegisterSerializerEx",
|
||
"LOGIN_SERIALIZER": "cvat.apps.iam.serializers.LoginSerializerEx",
|
||
"PASSWORD_RESET_SERIALIZER": "cvat.apps.iam.serializers.PasswordResetSerializerEx",
|
||
# Define password-setting serializers explicitly so CVAT controls length limits
|
||
# instead of inheriting hardcoded third-party defaults.
|
||
"PASSWORD_RESET_CONFIRM_SERIALIZER": "cvat.apps.iam.serializers.PasswordResetConfirmSerializerEx",
|
||
"PASSWORD_CHANGE_SERIALIZER": "cvat.apps.iam.serializers.PasswordChangeSerializerEx",
|
||
"OLD_PASSWORD_FIELD_ENABLED": True,
|
||
}
|
||
|
||
ANALYTICS_ENABLED = to_bool(os.getenv("CVAT_ANALYTICS", False))
|
||
|
||
if ANALYTICS_ENABLED:
|
||
INSTALLED_APPS += ["cvat.apps.log_viewer"]
|
||
|
||
MIDDLEWARE = [
|
||
"django.middleware.security.SecurityMiddleware",
|
||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||
"cvat.apps.iam.middleware.SessionRefreshMiddleware",
|
||
"corsheaders.middleware.CorsMiddleware",
|
||
"django.middleware.common.CommonMiddleware",
|
||
"django.middleware.csrf.CsrfViewMiddleware",
|
||
# FIXME
|
||
# 'corsheaders.middleware.CorsPostCsrfMiddleware',
|
||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||
"cvat.apps.iam.no_auth_middleware.NoAuthMiddleware",
|
||
"django.middleware.gzip.GZipMiddleware",
|
||
"cvat.apps.engine.middleware.RequestTrackingMiddleware",
|
||
"cvat.apps.engine.middleware.LastActivityMiddleware",
|
||
"crum.CurrentRequestUserMiddleware",
|
||
"django.contrib.messages.middleware.MessageMiddleware",
|
||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||
"dj_pagination.middleware.PaginationMiddleware",
|
||
"cvat.apps.iam.middleware.ContextMiddleware",
|
||
"allauth.account.middleware.AccountMiddleware",
|
||
]
|
||
|
||
# HSAP iframe 嵌入标注画布(跨端口 8787 → 8080)
|
||
X_FRAME_OPTIONS = "ALLOWALL"
|
||
|
||
UI_URL = ""
|
||
|
||
ROOT_URLCONF = "cvat.urls"
|
||
|
||
TEMPLATES = [
|
||
{
|
||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||
"DIRS": [],
|
||
"APP_DIRS": True,
|
||
"OPTIONS": {
|
||
"context_processors": [
|
||
"django.template.context_processors.debug",
|
||
"django.template.context_processors.request",
|
||
"django.contrib.auth.context_processors.auth",
|
||
"django.contrib.messages.context_processors.messages",
|
||
],
|
||
},
|
||
},
|
||
]
|
||
|
||
# IAM settings
|
||
IAM_TYPE = "BASIC"
|
||
IAM_BASE_EXCEPTION = None # a class which will be used by IAM to report errors
|
||
IAM_DEFAULT_ROLE = "user"
|
||
|
||
IAM_ADMIN_ROLE = "admin"
|
||
# Index in the list below corresponds to the priority (0 has highest priority)
|
||
IAM_ROLES = [IAM_ADMIN_ROLE, "user", "worker"]
|
||
IAM_OPA_URL = os.getenv("CVAT_OPA_URL", "http://opa:8181")
|
||
IAM_OPA_DATA_URL = f"{IAM_OPA_URL}/v1/data"
|
||
LOGIN_URL = "rest_login"
|
||
LOGIN_REDIRECT_URL = "/"
|
||
|
||
OBJECTS_NOT_RELATED_WITH_ORG = [
|
||
"user",
|
||
"lambda_function",
|
||
"lambda_request",
|
||
"server",
|
||
"request",
|
||
"access_token",
|
||
]
|
||
|
||
# ORG settings
|
||
ORG_INVITATION_CONFIRM = "No"
|
||
ORG_INVITATION_EXPIRY_DAYS = 7
|
||
|
||
|
||
AUTHENTICATION_BACKENDS = [
|
||
"django.contrib.auth.backends.ModelBackend",
|
||
"allauth.account.auth_backends.AuthenticationBackend",
|
||
]
|
||
|
||
# https://github.com/pennersr/django-allauth
|
||
ACCOUNT_EMAIL_VERIFICATION = "none"
|
||
ACCOUNT_AUTHENTICATION_METHOD = "username_email"
|
||
|
||
# set UI url to redirect after a successful e-mail confirmation
|
||
# changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message
|
||
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "/auth/email-confirmation"
|
||
ACCOUNT_EMAIL_VERIFICATION_SENT_REDIRECT_URL = "/auth/email-verification-sent"
|
||
INCORRECT_EMAIL_CONFIRMATION_URL = "/auth/incorrect-email-confirmation"
|
||
|
||
# Django-RQ
|
||
# https://github.com/rq/django-rq
|
||
|
||
|
||
class CVAT_QUEUES(Enum):
|
||
IMPORT_DATA = "import"
|
||
EXPORT_DATA = "export"
|
||
AUTO_ANNOTATION = "annotation"
|
||
WEBHOOKS = "webhooks"
|
||
NOTIFICATIONS = "notifications"
|
||
QUALITY_REPORTS = "quality_reports"
|
||
CLEANING = "cleaning"
|
||
CHUNKS = "chunks"
|
||
CONSENSUS = "consensus"
|
||
|
||
|
||
redis_inmem_host = os.getenv("CVAT_REDIS_INMEM_HOST", "localhost")
|
||
redis_inmem_port = os.getenv("CVAT_REDIS_INMEM_PORT", 6379)
|
||
redis_inmem_password = os.getenv("CVAT_REDIS_INMEM_PASSWORD", "")
|
||
|
||
|
||
class REDIS_INMEM_DATABASES(IntEnum):
|
||
RQ = 0
|
||
CACHE = 1
|
||
|
||
|
||
REDIS_INMEM_SETTINGS = {
|
||
"HOST": redis_inmem_host,
|
||
"PORT": redis_inmem_port,
|
||
"DB": REDIS_INMEM_DATABASES.RQ,
|
||
"PASSWORD": redis_inmem_password,
|
||
}
|
||
|
||
RQ_QUEUES = {
|
||
CVAT_QUEUES.IMPORT_DATA.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "4h",
|
||
# custom fields
|
||
"PARSED_JOB_ID_CLASS": "cvat.apps.engine.rq.ImportRequestId",
|
||
},
|
||
CVAT_QUEUES.EXPORT_DATA.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "4h",
|
||
# custom fields
|
||
"PARSED_JOB_ID_CLASS": "cvat.apps.engine.rq.ExportRequestId",
|
||
},
|
||
CVAT_QUEUES.AUTO_ANNOTATION.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "24h",
|
||
},
|
||
CVAT_QUEUES.WEBHOOKS.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "1h",
|
||
},
|
||
CVAT_QUEUES.NOTIFICATIONS.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "1h",
|
||
},
|
||
CVAT_QUEUES.QUALITY_REPORTS.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "1h",
|
||
# custom fields
|
||
"PARSED_JOB_ID_CLASS": "cvat.apps.quality_control.rq.QualityRequestId",
|
||
},
|
||
CVAT_QUEUES.CLEANING.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "2h",
|
||
},
|
||
CVAT_QUEUES.CHUNKS.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "5m",
|
||
},
|
||
CVAT_QUEUES.CONSENSUS.value: {
|
||
**REDIS_INMEM_SETTINGS,
|
||
"DEFAULT_TIMEOUT": "1h",
|
||
# custom fields
|
||
"PARSED_JOB_ID_CLASS": "cvat.apps.consensus.rq.ConsensusRequestId",
|
||
},
|
||
}
|
||
|
||
NUCLIO = {
|
||
"SCHEME": os.getenv("CVAT_NUCLIO_SCHEME", "http"),
|
||
"HOST": os.getenv("CVAT_NUCLIO_HOST", "localhost"),
|
||
"PORT": int(os.getenv("CVAT_NUCLIO_PORT", 8070)),
|
||
"DEFAULT_TIMEOUT": int(os.getenv("CVAT_NUCLIO_DEFAULT_TIMEOUT", 120)),
|
||
"FUNCTION_NAMESPACE": os.getenv("CVAT_NUCLIO_FUNCTION_NAMESPACE", "nuclio"),
|
||
"INVOKE_METHOD": os.getenv(
|
||
"CVAT_NUCLIO_INVOKE_METHOD",
|
||
default="dashboard" if "KUBERNETES_SERVICE_HOST" in os.environ else "direct",
|
||
),
|
||
}
|
||
|
||
assert NUCLIO["INVOKE_METHOD"] in {"dashboard", "direct"}
|
||
|
||
RQ_SHOW_ADMIN_LINK = True
|
||
RQ_EXCEPTION_HANDLERS = [
|
||
"cvat.apps.engine.views.rq_exception_handler",
|
||
"cvat.apps.events.handlers.handle_rq_exception",
|
||
]
|
||
|
||
PERIODIC_RQ_JOBS = [
|
||
{
|
||
"queue": CVAT_QUEUES.CLEANING.value,
|
||
"id": "clean_up_sessions",
|
||
"func": "cvat.apps.iam.utils.clean_up_sessions",
|
||
"cron_string": "0 0 * * *",
|
||
},
|
||
{
|
||
"queue": CVAT_QUEUES.CLEANING.value,
|
||
"id": "cron_export_cache_directory_cleanup",
|
||
"func": "cvat.apps.dataset_manager.cron.cleanup_export_cache_directory",
|
||
# Run twice a day (at midnight and at noon)
|
||
"cron_string": "0 0,12 * * *",
|
||
},
|
||
{
|
||
"queue": CVAT_QUEUES.CLEANING.value,
|
||
"id": "cron_tmp_directory_cleanup",
|
||
"func": "cvat.apps.dataset_manager.cron.cleanup_tmp_directory",
|
||
# Run once a day
|
||
"cron_string": "0 18 * * *",
|
||
},
|
||
{
|
||
"queue": CVAT_QUEUES.CLEANING.value,
|
||
"id": "clear_unusable_access_tokens",
|
||
"func": "cvat.apps.access_tokens.cron.clear_unusable_access_tokens",
|
||
"cron_string": "0 0 * * 0",
|
||
},
|
||
]
|
||
|
||
# Password validation
|
||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||
|
||
AUTH_PASSWORD_VALIDATORS = [
|
||
{
|
||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||
},
|
||
{
|
||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||
"OPTIONS": {"min_length": DEFAULT_MIN_PASSWORD_LENGTH},
|
||
},
|
||
{
|
||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||
},
|
||
{
|
||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||
},
|
||
{
|
||
"NAME": "cvat.apps.iam.password_validation.MaximumLengthPasswordValidator",
|
||
},
|
||
]
|
||
|
||
# Internationalization
|
||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
||
|
||
LANGUAGE_CODE = "en-us"
|
||
|
||
TIME_ZONE = os.getenv("TZ", "Etc/UTC")
|
||
|
||
USE_I18N = True
|
||
|
||
USE_TZ = True
|
||
|
||
CSRF_COOKIE_NAME = "csrftoken"
|
||
|
||
# Static files (CSS, JavaScript, Images)
|
||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||
|
||
STATIC_URL = "/static/"
|
||
STATIC_ROOT = BASE_DIR / "static"
|
||
STATIC_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
# Make sure to update other config files when updating these directories
|
||
DATA_ROOT = BASE_DIR / "data"
|
||
|
||
MEDIA_DATA_ROOT = DATA_ROOT / "data"
|
||
MEDIA_DATA_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
CACHE_ROOT = DATA_ROOT / "cache"
|
||
CACHE_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
EXPORT_CACHE_ROOT = CACHE_ROOT / "export"
|
||
EXPORT_CACHE_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
EVENTS_LOCAL_DB_ROOT = BASE_DIR / "events"
|
||
EVENTS_LOCAL_DB_ROOT.mkdir(parents=True, exist_ok=True)
|
||
EVENTS_LOCAL_DB_FILE = Path(
|
||
EVENTS_LOCAL_DB_ROOT,
|
||
os.getenv("CVAT_EVENTS_LOCAL_DB_FILENAME", "events.db"),
|
||
)
|
||
EVENTS_LOCAL_DB_FILE.touch(exist_ok=True)
|
||
|
||
JOBS_ROOT = DATA_ROOT / "jobs"
|
||
JOBS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
TASKS_ROOT = DATA_ROOT / "tasks"
|
||
TASKS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
PROJECTS_ROOT = DATA_ROOT / "projects"
|
||
PROJECTS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
ASSETS_ROOT = DATA_ROOT / "assets"
|
||
ASSETS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
SHARE_ROOT = BASE_DIR / "share"
|
||
SHARE_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
LOGS_ROOT = BASE_DIR / "logs"
|
||
LOGS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
MIGRATIONS_LOGS_ROOT = LOGS_ROOT / "migrations"
|
||
MIGRATIONS_LOGS_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
CLOUD_STORAGE_ROOT = DATA_ROOT / "storages"
|
||
CLOUD_STORAGE_ROOT.mkdir(parents=True, exist_ok=True)
|
||
|
||
TMP_FILES_ROOT = DATA_ROOT / "tmp"
|
||
TMP_FILES_ROOT.mkdir(parents=True, exist_ok=True)
|
||
IGNORE_TMP_FOLDER_CLEANUP_ERRORS = True
|
||
|
||
# logging is known to be unreliable with RQ when using async transports
|
||
vector_log_handler = os.getenv("VECTOR_EVENT_HANDLER", "AsynchronousLogstashHandler")
|
||
|
||
logstash_async_constants.QUEUED_EVENTS_FLUSH_INTERVAL = 2.0
|
||
LOGGING = {
|
||
"version": 1,
|
||
"disable_existing_loggers": False,
|
||
"formatters": {
|
||
"vector": {"format": "%(message)s"},
|
||
"standard": {"format": "[%(asctime)s] %(levelname)s %(name)s: %(message)s"},
|
||
},
|
||
"handlers": {
|
||
"console": {
|
||
"class": "logging.StreamHandler",
|
||
"filters": [],
|
||
"formatter": "standard",
|
||
},
|
||
"server_file": {
|
||
"class": "logging.handlers.RotatingFileHandler",
|
||
"level": "DEBUG",
|
||
"filename": LOGS_ROOT / "cvat_server.log",
|
||
"formatter": "standard",
|
||
"maxBytes": 1024 * 1024 * 50, # 50 MB
|
||
"backupCount": 5,
|
||
},
|
||
"dataset_handler": {
|
||
"class": "logging.handlers.RotatingFileHandler",
|
||
"level": "DEBUG",
|
||
"filename": LOGS_ROOT / "cvat_server_dataset.log",
|
||
"formatter": "standard",
|
||
"maxBytes": 1024 * 1024 * 50, # 50 MB
|
||
"backupCount": 3,
|
||
},
|
||
"vector": {
|
||
"level": "INFO",
|
||
"class": f"logstash_async.handler.{vector_log_handler}",
|
||
"formatter": "vector",
|
||
"transport": "logstash_async.transport.HttpTransport",
|
||
"ssl_enable": False,
|
||
"ssl_verify": False,
|
||
"host": os.getenv("DJANGO_LOG_SERVER_HOST", "localhost"),
|
||
"port": os.getenv("DJANGO_LOG_SERVER_PORT", 8282),
|
||
"version": 1,
|
||
"message_type": "django",
|
||
"database_path": EVENTS_LOCAL_DB_FILE,
|
||
},
|
||
},
|
||
"root": {
|
||
"handlers": ["console", "server_file"],
|
||
},
|
||
"loggers": {
|
||
"cvat": {
|
||
"level": os.getenv("DJANGO_LOG_LEVEL", "DEBUG"),
|
||
},
|
||
"dataset_logger": {
|
||
"handlers": ["dataset_handler"],
|
||
},
|
||
"django": {
|
||
"level": "INFO",
|
||
},
|
||
"vector": {
|
||
"handlers": [],
|
||
"level": "INFO",
|
||
# set True for debug
|
||
"propagate": False,
|
||
},
|
||
},
|
||
}
|
||
|
||
CVAT_LOG_IMPORT_ERRORS = to_bool(os.getenv("CVAT_LOG_IMPORT_ERRORS", False))
|
||
|
||
if os.getenv("DJANGO_LOG_SERVER_HOST"):
|
||
LOGGING["loggers"]["vector"]["handlers"] += ["vector"]
|
||
|
||
DATA_UPLOAD_MAX_MEMORY_SIZE = 100 * 1024 * 1024 # 100 MB
|
||
DATA_UPLOAD_MAX_NUMBER_FIELDS = None # this django check disabled
|
||
DATA_UPLOAD_MAX_NUMBER_FILES = None
|
||
|
||
redis_ondisk_host = os.getenv("CVAT_REDIS_ONDISK_HOST", "localhost")
|
||
# The default port is not Redis's default port (6379).
|
||
# This is so that a developer can run both in-mem Redis and on-disk Kvrocks on their machine
|
||
# without running into a port conflict.
|
||
redis_ondisk_port = os.getenv("CVAT_REDIS_ONDISK_PORT", 6666)
|
||
redis_ondisk_password = os.getenv("CVAT_REDIS_ONDISK_PASSWORD", "")
|
||
|
||
# Sets the timeout for the expiration of data chunk in redis_ondisk
|
||
CVAT_CHUNK_CACHE_TTL = 3600 * 24 # 1 day
|
||
|
||
# Sets the timeout for the expiration of preview image in redis_ondisk
|
||
CVAT_PREVIEW_CACHE_TTL = 3600 * 24 * 7 # 7 days
|
||
|
||
CACHES = {
|
||
"default": {
|
||
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||
"LOCATION": f"redis://:{urllib.parse.quote(redis_inmem_password)}@{redis_inmem_host}:{redis_inmem_port}/{REDIS_INMEM_DATABASES.CACHE}",
|
||
},
|
||
"media": {
|
||
"BACKEND": "django.core.cache.backends.redis.RedisCache",
|
||
"LOCATION": f"redis://:{urllib.parse.quote(redis_ondisk_password)}@{redis_ondisk_host}:{redis_ondisk_port}",
|
||
"TIMEOUT": CVAT_CHUNK_CACHE_TTL,
|
||
},
|
||
}
|
||
|
||
USE_CACHE = True
|
||
|
||
CORS_ALLOW_HEADERS = list(default_headers) + [
|
||
# tus upload protocol headers
|
||
"upload-offset",
|
||
"upload-length",
|
||
"tus-version",
|
||
"tus-resumable",
|
||
# extended upload protocol headers
|
||
"upload-start",
|
||
"upload-finish",
|
||
"upload-multiple",
|
||
"x-organization",
|
||
]
|
||
|
||
CORS_EXPOSE_HEADERS = [
|
||
"Content-Range",
|
||
]
|
||
|
||
TUS_MAX_FILE_SIZE = 26843545600 # 25gb
|
||
|
||
# This setting makes request secure if X-Forwarded-Proto: 'https' header is specified by our proxy
|
||
# More about forwarded headers - https://doc.traefik.io/traefik/getting-started/faq/#what-are-the-forwarded-headers-when-proxying-http-requests
|
||
# How django uses X-Forwarded-Proto - https://docs.djangoproject.com/en/2.2/ref/settings/#secure-proxy-ssl-header
|
||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||
|
||
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
|
||
|
||
# Forwarded host - https://docs.djangoproject.com/en/4.0/ref/settings/#std:setting-USE_X_FORWARDED_HOST
|
||
# Is used in TUS uploads to provide correct upload endpoint
|
||
USE_X_FORWARDED_HOST = True
|
||
|
||
# Django-sendfile requires to set SENDFILE_ROOT
|
||
# https://github.com/moggers87/django-sendfile2
|
||
SENDFILE_ROOT = BASE_DIR
|
||
|
||
CVAT_DOCS_URL = "https://docs.cvat.ai/docs/"
|
||
|
||
SPECTACULAR_SETTINGS = {
|
||
"TITLE": "CVAT REST API",
|
||
"DESCRIPTION": "REST API for Computer Vision Annotation Tool (CVAT)",
|
||
# Statically set schema version. May also be an empty string. When used together with
|
||
# view versioning, will become '0.0.0 (v2)' for 'v2' versioned requests.
|
||
# Set VERSION to None if only the request version should be rendered.
|
||
"VERSION": __version__,
|
||
"CONTACT": {
|
||
"name": "CVAT.ai team",
|
||
"url": "https://github.com/cvat-ai/cvat",
|
||
"email": "support@cvat.ai",
|
||
},
|
||
"LICENSE": {
|
||
"name": "MIT License",
|
||
"url": "https://en.wikipedia.org/wiki/MIT_License",
|
||
},
|
||
"SERVE_PUBLIC": True,
|
||
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
|
||
# https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/
|
||
"SWAGGER_UI_SETTINGS": {
|
||
"deepLinking": True,
|
||
"displayOperationId": True,
|
||
"displayRequestDuration": True,
|
||
"filter": True,
|
||
"showExtensions": True,
|
||
},
|
||
"TOS": "https://www.google.com/policies/terms/",
|
||
"EXTERNAL_DOCS": {
|
||
"description": "CVAT documentation",
|
||
"url": CVAT_DOCS_URL,
|
||
},
|
||
# OTHER SETTINGS
|
||
# https://drf-spectacular.readthedocs.io/en/latest/settings.html
|
||
#
|
||
# TODO: Our current implementation does not suppose this.
|
||
# Need to reconsider this later. It happens, for example,
|
||
# in TaskSerializer for data-originated fields - they can be empty.
|
||
# https://github.com/tfranzel/drf-spectacular/issues/54
|
||
"COMPONENT_NO_READ_ONLY_REQUIRED": True,
|
||
# Required for correct file upload type (bytes)
|
||
"COMPONENT_SPLIT_REQUEST": True,
|
||
"ENUM_NAME_OVERRIDES": {
|
||
"LabelType": "cvat.apps.engine.models.LabelType",
|
||
"ShapeType": "cvat.apps.engine.models.ShapeType",
|
||
"OperationStatus": "cvat.apps.engine.models.StateChoice",
|
||
"ChunkType": "cvat.apps.engine.models.DataChoice",
|
||
"MediaType": "cvat.apps.engine.models.MediaType",
|
||
"Dimension": "cvat.apps.engine.models.DimensionType",
|
||
"StorageMethod": "cvat.apps.engine.models.StorageMethodChoice",
|
||
"JobStatus": "cvat.apps.engine.models.StatusChoice",
|
||
"JobStage": "cvat.apps.engine.models.StageChoice",
|
||
"JobType": "cvat.apps.engine.models.JobType",
|
||
"TaskMode": "cvat.apps.engine.models.TaskMode",
|
||
"StorageType": "cvat.apps.engine.models.StorageChoice",
|
||
"SortingMethod": "cvat.apps.engine.models.SortingMethod",
|
||
"WebhookType": "cvat.apps.webhooks.models.WebhookTypeChoice",
|
||
"WebhookContentType": "cvat.apps.webhooks.models.WebhookContentTypeChoice",
|
||
"RequestStatus": "cvat.apps.redis_handler.serializers.RequestStatus",
|
||
"ValidationMode": "cvat.apps.engine.models.ValidationMode",
|
||
"FrameSelectionMethod": "cvat.apps.engine.models.JobFrameSelectionMethod",
|
||
"AnnotationConflictType": "cvat.apps.quality_control.models.AnnotationConflictType",
|
||
"AnnotationConflictSeverity": "cvat.apps.quality_control.models.AnnotationConflictSeverity",
|
||
"AnnotationConflictAnnotationType": "cvat.apps.quality_control.models.AnnotationType",
|
||
"MismatchingAnnotationKind": "cvat.apps.quality_control.models.MismatchingAnnotationKind",
|
||
"QualityTargetMetric": "cvat.apps.quality_control.models.QualityTargetMetricType",
|
||
"QualityPointSizeBase": "cvat.apps.quality_control.models.PointSizeBase",
|
||
"QualityReportTarget": "cvat.apps.quality_control.models.QualityReportTarget",
|
||
},
|
||
# Coercion of {pk} to {id} is controlled by SCHEMA_COERCE_PATH_PK. Additionally,
|
||
# some libraries (e.g. drf-nested-routers) use "_pk" suffixed path variables.
|
||
# This setting globally coerces path variables like "{user_pk}" to "{user_id}".
|
||
"SCHEMA_COERCE_PATH_PK_SUFFIX": True,
|
||
"SCHEMA_PATH_PREFIX": "/api",
|
||
"SCHEMA_PATH_PREFIX_TRIM": False,
|
||
"GENERIC_ADDITIONAL_PROPERTIES": None,
|
||
}
|
||
|
||
# set similar UI restrictions
|
||
# https://github.com/cvat-ai/cvat/blob/bad1dc2799afbb22222faaecc7336d999f4cc3fe/cvat-ui/src/utils/validation-patterns.ts#L26
|
||
ACCOUNT_USERNAME_MIN_LENGTH = 5
|
||
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
|
||
|
||
ACCOUNT_ADAPTER = "cvat.apps.iam.adapters.DefaultAccountAdapterEx"
|
||
|
||
CVAT_HOST = os.getenv("CVAT_HOST", "localhost")
|
||
CVAT_BASE_URL = os.getenv("CVAT_BASE_URL", f"http://{CVAT_HOST}:8080").rstrip("/")
|
||
|
||
CLICKHOUSE = {
|
||
"events": {
|
||
"NAME": os.getenv("CLICKHOUSE_DB", "cvat"),
|
||
"HOST": os.getenv("CLICKHOUSE_HOST", "localhost"),
|
||
"PORT": os.getenv("CLICKHOUSE_PORT", 8123),
|
||
"USER": os.getenv("CLICKHOUSE_USER", "user"),
|
||
"PASSWORD": os.getenv("CLICKHOUSE_PASSWORD", "user"),
|
||
}
|
||
}
|
||
|
||
if (postgres_password_file := os.getenv("CVAT_POSTGRES_PASSWORD_FILE")) is not None:
|
||
if "CVAT_POSTGRES_PASSWORD" in os.environ:
|
||
raise ImproperlyConfigured(
|
||
"The CVAT_POSTGRES_PASSWORD and CVAT_POSTGRES_PASSWORD_FILE"
|
||
" environment variables must not be set at the same time"
|
||
)
|
||
|
||
postgres_password = Path(postgres_password_file).read_text(encoding="UTF-8").rstrip("\n")
|
||
else:
|
||
postgres_password = os.getenv("CVAT_POSTGRES_PASSWORD", "")
|
||
|
||
# Database
|
||
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||
DATABASES = {
|
||
"default": {
|
||
"ENGINE": "django.db.backends.postgresql",
|
||
"HOST": os.getenv("CVAT_POSTGRES_HOST", "cvat_db"),
|
||
"NAME": os.getenv("CVAT_POSTGRES_DBNAME", "cvat"),
|
||
"USER": os.getenv("CVAT_POSTGRES_USER", "root"),
|
||
"PASSWORD": postgres_password,
|
||
"PORT": os.getenv("CVAT_POSTGRES_PORT", 5432),
|
||
"OPTIONS": {
|
||
"application_name": os.getenv("CVAT_POSTGRES_APPLICATION_NAME", "cvat"),
|
||
},
|
||
}
|
||
}
|
||
|
||
BUCKET_CONTENT_MAX_PAGE_SIZE = 500
|
||
|
||
IMPORT_CACHE_FAILED_TTL = timedelta(days=30)
|
||
IMPORT_CACHE_SUCCESS_TTL = timedelta(hours=1)
|
||
IMPORT_CACHE_CLEAN_DELAY = timedelta(hours=12)
|
||
|
||
ASSET_MAX_SIZE_MB = 10
|
||
ASSET_SUPPORTED_TYPES = ("image/jpeg", "image/png", "image/webp", "image/gif", "application/pdf")
|
||
ASSET_MAX_IMAGE_SIZE = 1920
|
||
ASSET_MAX_COUNT_PER_GUIDE = 150
|
||
|
||
SMOKESCREEN_ENABLED = to_bool(os.getenv("SMOKESCREEN_ENABLED", True))
|
||
|
||
# By default, email backend is django.core.mail.backends.smtp.EmailBackend
|
||
# But it won't work without additional configuration, so we set it to None
|
||
# to check configuration and throw ImproperlyConfigured if thats a case
|
||
EMAIL_BACKEND = None
|
||
|
||
ONE_RUNNING_JOB_IN_QUEUE_PER_USER = to_bool(os.getenv("ONE_RUNNING_JOB_IN_QUEUE_PER_USER", False))
|
||
|
||
# How many chunks can be prepared simultaneously during task creation in case the cache is not used
|
||
CVAT_CONCURRENT_CHUNK_PROCESSING = int(os.getenv("CVAT_CONCURRENT_CHUNK_PROCESSING", 1))
|
||
|
||
from cvat.rq_patching import patch_rq
|
||
|
||
patch_rq()
|
||
|
||
CLOUD_DATA_DOWNLOADING_MAX_THREADS_NUMBER_PER_CPU = 4
|
||
|
||
# Indicates the maximum number of days a file or directory is retained in the temporary directory
|
||
TMP_FILE_OR_DIR_RETENTION_DAYS = 3
|
||
|
||
LOGO_FILENAME = "logo.svg"
|
||
ABOUT_INFO = {
|
||
"subtitle": "Open Data Annotation Platform",
|
||
}
|
||
|
||
if ONE_RUNNING_JOB_IN_QUEUE_PER_USER:
|
||
PERIODIC_RQ_JOBS.append(
|
||
{
|
||
"queue": CVAT_QUEUES.CLEANING.value,
|
||
"id": "cleanup_deferred_job_registry",
|
||
"func": "cvat.apps.redis_handler.cron.cleanup_deferred_job_registry",
|
||
"cron_string": "0 8 * * *",
|
||
}
|
||
)
|
||
|
||
USER_LAST_ACTIVITY_UPDATE_MIN_INTERVAL = timedelta(days=1)
|
||
|
||
# Health check settings
|
||
HEALTH_CHECK = {"DISK_USAGE_MAX": int(os.getenv("CVAT_HEALTH_DISK_USAGE_MAX", 90))}
|