settings.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. __package__ = 'archivebox.core'
  2. import os
  3. import sys
  4. import re
  5. import logging
  6. import inspect
  7. import tempfile
  8. from typing import Any, Dict
  9. from pathlib import Path
  10. from django.utils.crypto import get_random_string
  11. from ..config import CONFIG
  12. from ..config_stubs import AttrDict
  13. assert isinstance(CONFIG, AttrDict)
  14. IS_MIGRATING = 'makemigrations' in sys.argv[:3] or 'migrate' in sys.argv[:3]
  15. IS_TESTING = 'test' in sys.argv[:3] or 'PYTEST_CURRENT_TEST' in os.environ
  16. IS_SHELL = 'shell' in sys.argv[:3] or 'shell_plus' in sys.argv[:3]
  17. ################################################################################
  18. ### Django Core Settings
  19. ################################################################################
  20. WSGI_APPLICATION = 'core.wsgi.application'
  21. ROOT_URLCONF = 'core.urls'
  22. LOGIN_URL = '/accounts/login/'
  23. LOGOUT_REDIRECT_URL = os.environ.get('LOGOUT_REDIRECT_URL', '/')
  24. PASSWORD_RESET_URL = '/accounts/password_reset/'
  25. APPEND_SLASH = True
  26. DEBUG = CONFIG.DEBUG or ('--debug' in sys.argv)
  27. BUILTIN_PLUGINS_DIR = CONFIG.PACKAGE_DIR / 'builtin_plugins'
  28. USER_PLUGINS_DIR = CONFIG.OUTPUT_DIR / 'user_plugins'
  29. def find_plugins(plugins_dir, prefix: str) -> Dict[str, Any]:
  30. plugins = {
  31. f'{prefix}.{plugin_entrypoint.parent.name}': plugin_entrypoint.parent
  32. for plugin_entrypoint in plugins_dir.glob('*/apps.py')
  33. }
  34. # print(f'Found {prefix} plugins:\n', '\n '.join(plugins.keys()))
  35. return plugins
  36. INSTALLED_PLUGINS = {
  37. **find_plugins(BUILTIN_PLUGINS_DIR, prefix='builtin_plugins'),
  38. **find_plugins(USER_PLUGINS_DIR, prefix='user_plugins'),
  39. }
  40. INSTALLED_APPS = [
  41. 'django.contrib.auth',
  42. 'django.contrib.contenttypes',
  43. 'django.contrib.sessions',
  44. 'django.contrib.messages',
  45. 'django.contrib.staticfiles',
  46. 'django.contrib.admin',
  47. 'django_jsonform',
  48. 'signal_webhooks',
  49. 'abid_utils',
  50. 'plugantic',
  51. 'core',
  52. 'api',
  53. 'pkgs',
  54. *INSTALLED_PLUGINS.keys(),
  55. 'admin_data_views',
  56. 'django_extensions',
  57. ]
  58. # For usage with https://www.jetadmin.io/integrations/django
  59. # INSTALLED_APPS += ['jet_django']
  60. # JET_PROJECT = 'archivebox'
  61. # JET_TOKEN = 'some-api-token-here'
  62. MIDDLEWARE = [
  63. 'core.middleware.TimezoneMiddleware',
  64. 'django.middleware.security.SecurityMiddleware',
  65. 'django.contrib.sessions.middleware.SessionMiddleware',
  66. 'django.middleware.common.CommonMiddleware',
  67. 'django.middleware.csrf.CsrfViewMiddleware',
  68. 'django.contrib.auth.middleware.AuthenticationMiddleware',
  69. 'core.middleware.ReverseProxyAuthMiddleware',
  70. 'django.contrib.messages.middleware.MessageMiddleware',
  71. 'core.middleware.CacheControlMiddleware',
  72. ]
  73. ################################################################################
  74. ### Authentication Settings
  75. ################################################################################
  76. # AUTH_USER_MODEL = 'auth.User' # cannot be easily changed unfortunately
  77. AUTHENTICATION_BACKENDS = [
  78. 'django.contrib.auth.backends.RemoteUserBackend',
  79. 'django.contrib.auth.backends.ModelBackend',
  80. ]
  81. if CONFIG.LDAP:
  82. try:
  83. import ldap
  84. from django_auth_ldap.config import LDAPSearch
  85. global AUTH_LDAP_SERVER_URI
  86. global AUTH_LDAP_BIND_DN
  87. global AUTH_LDAP_BIND_PASSWORD
  88. global AUTH_LDAP_USER_SEARCH
  89. global AUTH_LDAP_USER_ATTR_MAP
  90. AUTH_LDAP_SERVER_URI = CONFIG.LDAP_SERVER_URI
  91. AUTH_LDAP_BIND_DN = CONFIG.LDAP_BIND_DN
  92. AUTH_LDAP_BIND_PASSWORD = CONFIG.LDAP_BIND_PASSWORD
  93. assert AUTH_LDAP_SERVER_URI and CONFIG.LDAP_USERNAME_ATTR and CONFIG.LDAP_USER_FILTER, 'LDAP_* config options must all be set if LDAP=True'
  94. AUTH_LDAP_USER_SEARCH = LDAPSearch(
  95. CONFIG.LDAP_USER_BASE,
  96. ldap.SCOPE_SUBTREE,
  97. '(&(' + CONFIG.LDAP_USERNAME_ATTR + '=%(user)s)' + CONFIG.LDAP_USER_FILTER + ')',
  98. )
  99. AUTH_LDAP_USER_ATTR_MAP = {
  100. 'username': CONFIG.LDAP_USERNAME_ATTR,
  101. 'first_name': CONFIG.LDAP_FIRSTNAME_ATTR,
  102. 'last_name': CONFIG.LDAP_LASTNAME_ATTR,
  103. 'email': CONFIG.LDAP_EMAIL_ATTR,
  104. }
  105. AUTHENTICATION_BACKENDS = [
  106. 'django.contrib.auth.backends.ModelBackend',
  107. 'django_auth_ldap.backend.LDAPBackend',
  108. ]
  109. except ModuleNotFoundError:
  110. sys.stderr.write('[X] Error: Found LDAP=True config but LDAP packages not installed. You may need to run: pip install archivebox[ldap]\n\n')
  111. # dont hard exit here. in case the user is just running "archivebox version" or "archivebox help", we still want those to work despite broken ldap
  112. # sys.exit(1)
  113. ################################################################################
  114. ### Staticfile and Template Settings
  115. ################################################################################
  116. STATIC_URL = '/static/'
  117. STATICFILES_DIRS = [
  118. *([str(CONFIG.CUSTOM_TEMPLATES_DIR / 'static')] if CONFIG.CUSTOM_TEMPLATES_DIR else []),
  119. str(Path(CONFIG.PACKAGE_DIR) / CONFIG.TEMPLATES_DIR_NAME / 'static'),
  120. ]
  121. TEMPLATE_DIRS = [
  122. *([str(CONFIG.CUSTOM_TEMPLATES_DIR)] if CONFIG.CUSTOM_TEMPLATES_DIR else []),
  123. str(Path(CONFIG.PACKAGE_DIR) / CONFIG.TEMPLATES_DIR_NAME / 'core'),
  124. str(Path(CONFIG.PACKAGE_DIR) / CONFIG.TEMPLATES_DIR_NAME / 'admin'),
  125. str(Path(CONFIG.PACKAGE_DIR) / CONFIG.TEMPLATES_DIR_NAME),
  126. ]
  127. TEMPLATES = [
  128. {
  129. 'BACKEND': 'django.template.backends.django.DjangoTemplates',
  130. 'DIRS': TEMPLATE_DIRS,
  131. 'APP_DIRS': True,
  132. 'OPTIONS': {
  133. 'context_processors': [
  134. 'django.template.context_processors.debug',
  135. 'django.template.context_processors.request',
  136. 'django.contrib.auth.context_processors.auth',
  137. 'django.contrib.messages.context_processors.messages',
  138. ],
  139. },
  140. },
  141. ]
  142. ################################################################################
  143. ### External Service Settings
  144. ################################################################################
  145. CACHE_DB_FILENAME = 'cache.sqlite3'
  146. CACHE_DB_PATH = CONFIG.CACHE_DIR / CACHE_DB_FILENAME
  147. CACHE_DB_TABLE = 'django_cache'
  148. DATABASE_FILE = Path(CONFIG.OUTPUT_DIR) / CONFIG.SQL_INDEX_FILENAME
  149. DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", str(DATABASE_FILE))
  150. DATABASES = {
  151. 'default': {
  152. 'ENGINE': 'django.db.backends.sqlite3',
  153. 'NAME': DATABASE_NAME,
  154. 'OPTIONS': {
  155. 'timeout': 60,
  156. 'check_same_thread': False,
  157. },
  158. 'TIME_ZONE': CONFIG.TIMEZONE,
  159. # DB setup is sometimes modified at runtime by setup_django() in config.py
  160. },
  161. # 'cache': {
  162. # 'ENGINE': 'django.db.backends.sqlite3',
  163. # 'NAME': CACHE_DB_PATH,
  164. # 'OPTIONS': {
  165. # 'timeout': 60,
  166. # 'check_same_thread': False,
  167. # },
  168. # 'TIME_ZONE': CONFIG.TIMEZONE,
  169. # },
  170. }
  171. MIGRATION_MODULES = {'signal_webhooks': None}
  172. # as much as I'd love this to be a UUID or ULID field, it's not supported yet as of Django 5.0
  173. DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
  174. CACHES = {
  175. 'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'},
  176. # 'sqlite': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'cache'},
  177. # 'dummy': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
  178. # 'filebased': {"BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "LOCATION": CACHE_DIR / 'cache_filebased'},
  179. }
  180. EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
  181. STORAGES = {
  182. "default": {
  183. "BACKEND": "django.core.files.storage.FileSystemStorage",
  184. },
  185. "staticfiles": {
  186. "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
  187. },
  188. "archive": {
  189. "BACKEND": "django.core.files.storage.FileSystemStorage",
  190. "OPTIONS": {
  191. "base_url": "/archive/",
  192. "location": CONFIG.ARCHIVE_DIR,
  193. },
  194. },
  195. # "personas": {
  196. # "BACKEND": "django.core.files.storage.FileSystemStorage",
  197. # "OPTIONS": {
  198. # "base_url": "/personas/",
  199. # "location": PERSONAS_DIR,
  200. # },
  201. # },
  202. }
  203. ################################################################################
  204. ### Security Settings
  205. ################################################################################
  206. SECRET_KEY = CONFIG.SECRET_KEY or get_random_string(50, 'abcdefghijklmnopqrstuvwxyz0123456789_')
  207. ALLOWED_HOSTS = CONFIG.ALLOWED_HOSTS.split(',')
  208. CSRF_TRUSTED_ORIGINS = list(set(CONFIG.CSRF_TRUSTED_ORIGINS.split(',')))
  209. # automatically fix case when user sets ALLOWED_HOSTS (e.g. to archivebox.example.com)
  210. # but forgets to add https://archivebox.example.com to CSRF_TRUSTED_ORIGINS
  211. for hostname in ALLOWED_HOSTS:
  212. https_endpoint = f'https://{hostname}'
  213. if hostname != '*' and https_endpoint not in CSRF_TRUSTED_ORIGINS:
  214. print(f'[!] WARNING: {https_endpoint} from ALLOWED_HOSTS should be added to CSRF_TRUSTED_ORIGINS')
  215. CSRF_TRUSTED_ORIGINS.append(https_endpoint)
  216. SECURE_BROWSER_XSS_FILTER = True
  217. SECURE_CONTENT_TYPE_NOSNIFF = True
  218. SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
  219. CSRF_COOKIE_SECURE = False
  220. SESSION_COOKIE_SECURE = False
  221. SESSION_COOKIE_DOMAIN = None
  222. SESSION_COOKIE_AGE = 1209600 # 2 weeks
  223. SESSION_EXPIRE_AT_BROWSER_CLOSE = False
  224. SESSION_SAVE_EVERY_REQUEST = True
  225. SESSION_ENGINE = "django.contrib.sessions.backends.db"
  226. AUTH_PASSWORD_VALIDATORS = [
  227. {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
  228. {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
  229. {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
  230. {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
  231. ]
  232. DATA_UPLOAD_MAX_NUMBER_FIELDS = None
  233. ################################################################################
  234. ### Shell Settings
  235. ################################################################################
  236. SHELL_PLUS = 'ipython'
  237. SHELL_PLUS_PRINT_SQL = False
  238. IPYTHON_ARGUMENTS = ['--no-confirm-exit', '--no-banner']
  239. IPYTHON_KERNEL_DISPLAY_NAME = 'ArchiveBox Django Shell'
  240. if IS_SHELL:
  241. os.environ['PYTHONSTARTUP'] = str(Path(CONFIG.PACKAGE_DIR) / 'core' / 'welcome_message.py')
  242. ################################################################################
  243. ### Internationalization & Localization Settings
  244. ################################################################################
  245. LANGUAGE_CODE = 'en-us'
  246. USE_I18N = True
  247. USE_TZ = True
  248. DATETIME_FORMAT = 'Y-m-d g:iA'
  249. SHORT_DATETIME_FORMAT = 'Y-m-d h:iA'
  250. TIME_ZONE = CONFIG.TIMEZONE # django convention is TIME_ZONE, archivebox config uses TIMEZONE, they are equivalent
  251. from django.conf.locale.en import formats as en_formats # type: ignore
  252. en_formats.DATETIME_FORMAT = DATETIME_FORMAT
  253. en_formats.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT
  254. ################################################################################
  255. ### Logging Settings
  256. ################################################################################
  257. IGNORABLE_404_URLS = [
  258. re.compile(r'apple-touch-icon.*\.png$'),
  259. re.compile(r'favicon\.ico$'),
  260. re.compile(r'robots\.txt$'),
  261. re.compile(r'.*\.(css|js)\.map$'),
  262. ]
  263. IGNORABLE_200_URLS = [
  264. re.compile(r'^"GET /static/.* HTTP/.*" (200|30.) .+', re.I | re.M),
  265. re.compile(r'^"GET /admin/jsi18n/ HTTP/.*" (200|30.) .+', re.I | re.M),
  266. ]
  267. class NoisyRequestsFilter(logging.Filter):
  268. def filter(self, record) -> bool:
  269. logline = record.getMessage()
  270. # ignore harmless 404s for the patterns in IGNORABLE_404_URLS
  271. for ignorable_url_pattern in IGNORABLE_404_URLS:
  272. ignorable_log_pattern = re.compile(f'^"GET /.*/?{ignorable_url_pattern.pattern[:-1]} HTTP/.*" (200|30.|404) .+$', re.I | re.M)
  273. if ignorable_log_pattern.match(logline):
  274. return False
  275. ignorable_log_pattern = re.compile(f'^Not Found: /.*/?{ignorable_url_pattern.pattern}', re.I | re.M)
  276. if ignorable_log_pattern.match(logline):
  277. return False
  278. # ignore staticfile requests that 200 or 30*
  279. for ignorable_url_pattern in IGNORABLE_200_URLS:
  280. if ignorable_log_pattern.match(logline):
  281. return False
  282. return True
  283. ERROR_LOG = tempfile.NamedTemporaryFile().name
  284. if CONFIG.LOGS_DIR.exists():
  285. ERROR_LOG = (CONFIG.LOGS_DIR / 'errors.log')
  286. else:
  287. # historically too many edge cases here around creating log dir w/ correct permissions early on
  288. # if there's an issue on startup, we trash the log and let user figure it out via stdout/stderr
  289. print(f'[!] WARNING: data/logs dir does not exist. Logging to temp file: {ERROR_LOG}')
  290. LOGGING = {
  291. 'version': 1,
  292. 'disable_existing_loggers': False,
  293. 'handlers': {
  294. 'console': {
  295. 'class': 'logging.StreamHandler',
  296. },
  297. 'logfile': {
  298. 'level': 'ERROR',
  299. 'class': 'logging.handlers.RotatingFileHandler',
  300. 'filename': ERROR_LOG,
  301. 'maxBytes': 1024 * 1024 * 25, # 25 MB
  302. 'backupCount': 10,
  303. },
  304. },
  305. 'filters': {
  306. 'noisyrequestsfilter': {
  307. '()': NoisyRequestsFilter,
  308. }
  309. },
  310. 'loggers': {
  311. 'django': {
  312. 'handlers': ['console', 'logfile'],
  313. 'level': 'INFO',
  314. 'filters': ['noisyrequestsfilter'],
  315. },
  316. 'django.server': {
  317. 'handlers': ['console', 'logfile'],
  318. 'level': 'INFO',
  319. 'filters': ['noisyrequestsfilter'],
  320. }
  321. },
  322. }
  323. ################################################################################
  324. ### REST API Outbound Webhooks settings
  325. ################################################################################
  326. # Add default webhook configuration to the User model
  327. SIGNAL_WEBHOOKS_CUSTOM_MODEL = 'api.models.OutboundWebhook'
  328. SIGNAL_WEBHOOKS = {
  329. "HOOKS": {
  330. # ... is a special sigil value that means "use the default autogenerated hooks"
  331. "django.contrib.auth.models.User": ...,
  332. "core.models.Snapshot": ...,
  333. "core.models.ArchiveResult": ...,
  334. "core.models.Tag": ...,
  335. "api.models.APIToken": ...,
  336. },
  337. }
  338. ################################################################################
  339. ### Admin Data View Settings
  340. ################################################################################
  341. ADMIN_DATA_VIEWS = {
  342. "NAME": "Environment",
  343. "URLS": [
  344. {
  345. "route": "config/",
  346. "view": "core.views.live_config_list_view",
  347. "name": "Configuration",
  348. "items": {
  349. "route": "<str:key>/",
  350. "view": "core.views.live_config_value_view",
  351. "name": "config_val",
  352. },
  353. },
  354. {
  355. "route": "binaries/",
  356. "view": "plugantic.views.binaries_list_view",
  357. "name": "Binaries",
  358. "items": {
  359. "route": "<str:key>/",
  360. "view": "plugantic.views.binary_detail_view",
  361. "name": "binary",
  362. },
  363. },
  364. {
  365. "route": "plugins/",
  366. "view": "plugantic.views.plugins_list_view",
  367. "name": "Plugins",
  368. "items": {
  369. "route": "<str:key>/",
  370. "view": "plugantic.views.plugin_detail_view",
  371. "name": "plugin",
  372. },
  373. },
  374. ],
  375. }
  376. ################################################################################
  377. ### Debug Settings
  378. ################################################################################
  379. # only enable debug toolbar when in DEBUG mode with --nothreading (it doesnt work in multithreaded mode)
  380. DEBUG_TOOLBAR = False
  381. DEBUG_TOOLBAR = DEBUG_TOOLBAR and DEBUG and ('--nothreading' in sys.argv) and ('--reload' not in sys.argv)
  382. if DEBUG_TOOLBAR:
  383. try:
  384. import debug_toolbar # noqa
  385. DEBUG_TOOLBAR = True
  386. except ImportError:
  387. DEBUG_TOOLBAR = False
  388. if DEBUG_TOOLBAR:
  389. INSTALLED_APPS = [*INSTALLED_APPS, 'debug_toolbar']
  390. INTERNAL_IPS = ['0.0.0.0', '127.0.0.1', '*']
  391. DEBUG_TOOLBAR_CONFIG = {
  392. "SHOW_TOOLBAR_CALLBACK": lambda request: True,
  393. "RENDER_PANELS": True,
  394. }
  395. DEBUG_TOOLBAR_PANELS = [
  396. 'debug_toolbar.panels.history.HistoryPanel',
  397. 'debug_toolbar.panels.versions.VersionsPanel',
  398. 'debug_toolbar.panels.timer.TimerPanel',
  399. 'debug_toolbar.panels.settings.SettingsPanel',
  400. 'debug_toolbar.panels.headers.HeadersPanel',
  401. 'debug_toolbar.panels.request.RequestPanel',
  402. 'debug_toolbar.panels.sql.SQLPanel',
  403. 'debug_toolbar.panels.staticfiles.StaticFilesPanel',
  404. # 'debug_toolbar.panels.templates.TemplatesPanel',
  405. 'debug_toolbar.panels.cache.CachePanel',
  406. 'debug_toolbar.panels.signals.SignalsPanel',
  407. 'debug_toolbar.panels.logging.LoggingPanel',
  408. 'debug_toolbar.panels.redirects.RedirectsPanel',
  409. 'debug_toolbar.panels.profiling.ProfilingPanel',
  410. 'djdt_flamegraph.FlamegraphPanel',
  411. ]
  412. MIDDLEWARE = [*MIDDLEWARE, 'debug_toolbar.middleware.DebugToolbarMiddleware']
  413. if DEBUG:
  414. from django_autotyping.typing import AutotypingSettingsDict
  415. INSTALLED_APPS += ['django_autotyping']
  416. AUTOTYPING: AutotypingSettingsDict = {
  417. "STUBS_GENERATION": {
  418. "LOCAL_STUBS_DIR": Path(CONFIG.PACKAGE_DIR) / "typings",
  419. }
  420. }
  421. # https://github.com/bensi94/Django-Requests-Tracker (improved version of django-debug-toolbar)
  422. # Must delete archivebox/templates/admin to use because it relies on some things we override
  423. # visit /__requests_tracker__/ to access
  424. DEBUG_REQUESTS_TRACKER = True
  425. DEBUG_REQUESTS_TRACKER = DEBUG_REQUESTS_TRACKER and DEBUG
  426. if DEBUG_REQUESTS_TRACKER:
  427. import requests_tracker
  428. INSTALLED_APPS += ["requests_tracker"]
  429. MIDDLEWARE += ["requests_tracker.middleware.requests_tracker_middleware"]
  430. INTERNAL_IPS = ["127.0.0.1", "10.0.2.2", "0.0.0.0", "*"]
  431. TEMPLATE_DIRS.insert(0, str(Path(inspect.getfile(requests_tracker)).parent / "templates"))
  432. REQUESTS_TRACKER_CONFIG = {
  433. "TRACK_SQL": True,
  434. "ENABLE_STACKTRACES": False,
  435. "IGNORE_PATHS_PATTERNS": (
  436. r".*/favicon\.ico",
  437. r".*\.png",
  438. r"/admin/jsi18n/",
  439. ),
  440. "IGNORE_SQL_PATTERNS": (
  441. r"^SELECT .* FROM django_migrations WHERE app = 'requests_tracker'",
  442. r"^SELECT .* FROM django_migrations WHERE app = 'auth'",
  443. ),
  444. }
  445. # https://docs.pydantic.dev/logfire/integrations/django/ (similar to DataDog / NewRelic / etc.)
  446. DEBUG_LOGFIRE = False
  447. DEBUG_LOGFIRE = DEBUG_LOGFIRE and (Path(CONFIG.OUTPUT_DIR) / '.logfire').is_dir()