settings.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. __package__ = "archivebox.core"
  2. import os
  3. import sys
  4. import inspect
  5. from pathlib import Path
  6. from django.utils.crypto import get_random_string
  7. import archivebox
  8. from archivebox.config import DATA_DIR, PACKAGE_DIR, ARCHIVE_DIR, CONSTANTS # noqa
  9. from archivebox.config.common import SHELL_CONFIG, SERVER_CONFIG, STORAGE_CONFIG # noqa
  10. IS_MIGRATING = "makemigrations" in sys.argv[:3] or "migrate" in sys.argv[:3]
  11. IS_TESTING = "test" in sys.argv[:3] or "PYTEST_CURRENT_TEST" in os.environ
  12. IS_SHELL = "shell" in sys.argv[:3] or "shell_plus" in sys.argv[:3]
  13. IS_GETTING_VERSION_OR_HELP = "version" in sys.argv or "help" in sys.argv or "--version" in sys.argv or "--help" in sys.argv
  14. ################################################################################
  15. ### ArchiveBox Plugin Settings
  16. ################################################################################
  17. ALL_PLUGINS = archivebox.ALL_PLUGINS
  18. LOADED_PLUGINS = archivebox.LOADED_PLUGINS
  19. ################################################################################
  20. ### Django Core Settings
  21. ################################################################################
  22. WSGI_APPLICATION = "archivebox.core.wsgi.application"
  23. ASGI_APPLICATION = "archivebox.core.asgi.application"
  24. ROOT_URLCONF = "archivebox.core.urls"
  25. LOGIN_URL = "/accounts/login/"
  26. LOGOUT_REDIRECT_URL = os.environ.get("LOGOUT_REDIRECT_URL", "/")
  27. PASSWORD_RESET_URL = "/accounts/password_reset/"
  28. APPEND_SLASH = True
  29. DEBUG = SHELL_CONFIG.DEBUG or ("--debug" in sys.argv)
  30. INSTALLED_APPS = [
  31. "daphne",
  32. # Django default apps
  33. "django.contrib.auth",
  34. "django.contrib.contenttypes",
  35. "django.contrib.sessions",
  36. "django.contrib.messages",
  37. "django.contrib.staticfiles",
  38. "django.contrib.admin",
  39. # 3rd-party apps from PyPI
  40. "signal_webhooks", # handles REST API outbound webhooks https://github.com/MrThearMan/django-signal-webhooks
  41. "django_object_actions", # provides easy Django Admin action buttons on change views https://github.com/crccheck/django-object-actions
  42. # Our ArchiveBox-provided apps (use fully qualified names)
  43. # NOTE: Order matters! Apps with migrations that depend on other apps must come AFTER their dependencies
  44. # "archivebox.config", # ArchiveBox config settings (no models, not a real Django app)
  45. "archivebox.machine", # handles collecting and storing information about the host machine, network interfaces, binaries, etc.
  46. "archivebox.workers", # handles starting and managing background workers and processes (orchestrators and actors)
  47. "archivebox.personas", # handles Persona and session management
  48. "archivebox.core", # core django model with Snapshot, ArchiveResult, etc. (crawls depends on this)
  49. "archivebox.crawls", # handles Crawl and CrawlSchedule models and management (depends on core)
  50. "archivebox.api", # Django-Ninja-based Rest API interfaces, config, APIToken model, etc.
  51. # ArchiveBox plugins (hook-based plugins no longer add Django apps)
  52. # Use hooks.py discover_hooks() for plugin functionality
  53. # 3rd-party apps from PyPI that need to be loaded last
  54. "admin_data_views", # handles rendering some convenient automatic read-only views of data in Django admin
  55. "django_extensions", # provides Django Debug Toolbar (and other non-debug helpers)
  56. ]
  57. MIDDLEWARE = [
  58. "archivebox.core.middleware.TimezoneMiddleware",
  59. "django.middleware.security.SecurityMiddleware",
  60. "django.contrib.sessions.middleware.SessionMiddleware",
  61. "django.middleware.common.CommonMiddleware",
  62. "django.middleware.csrf.CsrfViewMiddleware",
  63. "django.contrib.auth.middleware.AuthenticationMiddleware",
  64. "archivebox.core.middleware.ReverseProxyAuthMiddleware",
  65. "django.contrib.messages.middleware.MessageMiddleware",
  66. "archivebox.core.middleware.CacheControlMiddleware",
  67. # Additional middlewares from plugins (if any)
  68. ]
  69. ################################################################################
  70. ### Authentication Settings
  71. ################################################################################
  72. # AUTH_USER_MODEL = 'auth.User' # cannot be easily changed unfortunately
  73. AUTHENTICATION_BACKENDS = [
  74. "django.contrib.auth.backends.RemoteUserBackend",
  75. "django.contrib.auth.backends.ModelBackend",
  76. # Additional auth backends (e.g., LDAP) configured via settings
  77. ]
  78. # from ..plugins_auth.ldap.settings import LDAP_CONFIG
  79. # if LDAP_CONFIG.LDAP_ENABLED:
  80. # AUTH_LDAP_BIND_DN = LDAP_CONFIG.LDAP_BIND_DN
  81. # AUTH_LDAP_SERVER_URI = LDAP_CONFIG.LDAP_SERVER_URI
  82. # AUTH_LDAP_BIND_PASSWORD = LDAP_CONFIG.LDAP_BIND_PASSWORD
  83. # AUTH_LDAP_USER_ATTR_MAP = LDAP_CONFIG.LDAP_USER_ATTR_MAP
  84. # AUTH_LDAP_USER_SEARCH = LDAP_CONFIG.AUTH_LDAP_USER_SEARCH
  85. # AUTHENTICATION_BACKENDS = LDAP_CONFIG.AUTHENTICATION_BACKENDS
  86. ################################################################################
  87. ### Staticfile and Template Settings
  88. ################################################################################
  89. STATIC_URL = "/static/"
  90. TEMPLATES_DIR_NAME = "templates"
  91. CUSTOM_TEMPLATES_ENABLED = os.path.isdir(STORAGE_CONFIG.CUSTOM_TEMPLATES_DIR) and os.access(STORAGE_CONFIG.CUSTOM_TEMPLATES_DIR, os.R_OK)
  92. STATICFILES_DIRS = [
  93. *([str(STORAGE_CONFIG.CUSTOM_TEMPLATES_DIR / "static")] if CUSTOM_TEMPLATES_ENABLED else []),
  94. # *[
  95. # str(plugin_dir / 'static')
  96. # for plugin_dir in PLUGIN_DIRS.values()
  97. # if (plugin_dir / 'static').is_dir()
  98. # ],
  99. # Additional static file dirs from plugins
  100. str(PACKAGE_DIR / TEMPLATES_DIR_NAME / "static"),
  101. ]
  102. TEMPLATE_DIRS = [
  103. *([str(STORAGE_CONFIG.CUSTOM_TEMPLATES_DIR)] if CUSTOM_TEMPLATES_ENABLED else []),
  104. # *[
  105. # str(plugin_dir / 'templates')
  106. # for plugin_dir in PLUGIN_DIRS.values()
  107. # if (plugin_dir / 'templates').is_dir()
  108. # ],
  109. # Additional template dirs from plugins
  110. str(PACKAGE_DIR / TEMPLATES_DIR_NAME / "core"),
  111. str(PACKAGE_DIR / TEMPLATES_DIR_NAME / "admin"),
  112. str(PACKAGE_DIR / TEMPLATES_DIR_NAME),
  113. ]
  114. TEMPLATES = [
  115. {
  116. "BACKEND": "django.template.backends.django.DjangoTemplates",
  117. "DIRS": TEMPLATE_DIRS,
  118. "APP_DIRS": True,
  119. "OPTIONS": {
  120. "context_processors": [
  121. "django.template.context_processors.debug",
  122. "django.template.context_processors.request",
  123. "django.contrib.auth.context_processors.auth",
  124. "django.contrib.messages.context_processors.messages",
  125. ],
  126. },
  127. },
  128. ]
  129. ################################################################################
  130. ### External Service Settings
  131. ################################################################################
  132. # CACHE_DB_FILENAME = 'cache.sqlite3'
  133. # CACHE_DB_PATH = CONSTANTS.CACHE_DIR / CACHE_DB_FILENAME
  134. # CACHE_DB_TABLE = 'django_cache'
  135. DATABASE_NAME = os.environ.get("ARCHIVEBOX_DATABASE_NAME", str(CONSTANTS.DATABASE_FILE))
  136. SQLITE_CONNECTION_OPTIONS = {
  137. "ENGINE": "django.db.backends.sqlite3",
  138. "TIME_ZONE": CONSTANTS.TIMEZONE,
  139. "OPTIONS": {
  140. # https://gcollazo.com/optimal-sqlite-settings-for-django/
  141. # https://litestream.io/tips/#busy-timeout
  142. # https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options
  143. "timeout": 10,
  144. "check_same_thread": False,
  145. "transaction_mode": "IMMEDIATE",
  146. "init_command": (
  147. "PRAGMA foreign_keys=ON;"
  148. "PRAGMA journal_mode = WAL;"
  149. "PRAGMA synchronous = NORMAL;"
  150. "PRAGMA temp_store = MEMORY;"
  151. "PRAGMA mmap_size = 134217728;"
  152. "PRAGMA journal_size_limit = 67108864;"
  153. "PRAGMA cache_size = 2000;"
  154. ),
  155. },
  156. }
  157. DATABASES = {
  158. "default": {
  159. "NAME": DATABASE_NAME,
  160. **SQLITE_CONNECTION_OPTIONS,
  161. },
  162. # "filestore": {
  163. # "NAME": CONSTANTS.FILESTORE_DATABASE_FILE,
  164. # **SQLITE_CONNECTION_OPTIONS,
  165. # },
  166. # 'cache': {
  167. # 'NAME': CACHE_DB_PATH,
  168. # **SQLITE_CONNECTION_OPTIONS,
  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. # class FilestoreDBRouter:
  175. # """
  176. # A router to store all the File models in the filestore.sqlite3 database.
  177. # This data just mirrors what is in the file system, so we want to keep it in a separate database
  178. # from the main index database to avoid contention.
  179. # """
  180. # route_app_labels = {"filestore"}
  181. # db_name = "filestore"
  182. # def db_for_read(self, model, **hints):
  183. # if model._meta.app_label in self.route_app_labels:
  184. # return self.db_name
  185. # return 'default'
  186. # def db_for_write(self, model, **hints):
  187. # if model._meta.app_label in self.route_app_labels:
  188. # return self.db_name
  189. # return 'default'
  190. # def allow_relation(self, obj1, obj2, **hints):
  191. # if obj1._meta.app_label in self.route_app_labels or obj2._meta.app_label in self.route_app_labels:
  192. # return obj1._meta.app_label == obj2._meta.app_label
  193. # return None
  194. # def allow_migrate(self, db, app_label, model_name=None, **hints):
  195. # if app_label in self.route_app_labels:
  196. # return db == self.db_name
  197. # return db == "default"
  198. DATABASE_ROUTERS = []
  199. CACHES = {
  200. "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"},
  201. # 'sqlite': {'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'cache'},
  202. # 'dummy': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
  203. # 'filebased': {"BACKEND": "django.core.cache.backends.filebased.FileBasedCache", "LOCATION": CACHE_DIR / 'cache_filebased'},
  204. }
  205. EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
  206. STORAGES = {
  207. "default": {
  208. "BACKEND": "django.core.files.storage.FileSystemStorage",
  209. },
  210. "staticfiles": {
  211. "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
  212. },
  213. "archive": {
  214. "BACKEND": "django.core.files.storage.FileSystemStorage",
  215. "OPTIONS": {
  216. "base_url": "/archive/",
  217. "location": ARCHIVE_DIR,
  218. },
  219. },
  220. # "snapshots": {
  221. # "BACKEND": "django.core.files.storage.FileSystemStorage",
  222. # "OPTIONS": {
  223. # "base_url": "/snapshots/",
  224. # "location": CONSTANTS.SNAPSHOTS_DIR,
  225. # },
  226. # },
  227. # "personas": {
  228. # "BACKEND": "django.core.files.storage.FileSystemStorage",
  229. # "OPTIONS": {
  230. # "base_url": "/personas/",
  231. # "location": PERSONAS_DIR,
  232. # },
  233. # },
  234. }
  235. CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}}
  236. ################################################################################
  237. ### Security Settings
  238. ################################################################################
  239. SECRET_KEY = SERVER_CONFIG.SECRET_KEY or get_random_string(50, "abcdefghijklmnopqrstuvwxyz0123456789_")
  240. ALLOWED_HOSTS = SERVER_CONFIG.ALLOWED_HOSTS.split(",")
  241. CSRF_TRUSTED_ORIGINS = list(set(SERVER_CONFIG.CSRF_TRUSTED_ORIGINS.split(",")))
  242. # automatically fix case when user sets ALLOWED_HOSTS (e.g. to archivebox.example.com)
  243. # but forgets to add https://archivebox.example.com to CSRF_TRUSTED_ORIGINS
  244. for hostname in ALLOWED_HOSTS:
  245. https_endpoint = f"https://{hostname}"
  246. if hostname != "*" and https_endpoint not in CSRF_TRUSTED_ORIGINS:
  247. print(f"[!] WARNING: {https_endpoint} from ALLOWED_HOSTS should be added to CSRF_TRUSTED_ORIGINS")
  248. CSRF_TRUSTED_ORIGINS.append(https_endpoint)
  249. SECURE_BROWSER_XSS_FILTER = True
  250. SECURE_CONTENT_TYPE_NOSNIFF = True
  251. SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
  252. CSRF_COOKIE_SECURE = False
  253. SESSION_COOKIE_SECURE = False
  254. SESSION_COOKIE_HTTPONLY = True
  255. SESSION_COOKIE_DOMAIN = None
  256. SESSION_COOKIE_AGE = 1209600 # 2 weeks
  257. SESSION_EXPIRE_AT_BROWSER_CLOSE = False
  258. SESSION_SAVE_EVERY_REQUEST = False
  259. SESSION_ENGINE = "django.contrib.sessions.backends.db"
  260. AUTH_PASSWORD_VALIDATORS = [
  261. {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
  262. {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
  263. {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
  264. {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
  265. ]
  266. DATA_UPLOAD_MAX_NUMBER_FIELDS = None
  267. DATA_UPLOAD_MAX_MEMORY_SIZE = 26_214_400 # 25MB
  268. ################################################################################
  269. ### Shell Settings
  270. ################################################################################
  271. SHELL_PLUS = "ipython"
  272. SHELL_PLUS_PRINT_SQL = False
  273. IPYTHON_ARGUMENTS = ["--no-confirm-exit", "--no-banner"]
  274. IPYTHON_KERNEL_DISPLAY_NAME = "ArchiveBox Django Shell"
  275. if IS_SHELL:
  276. os.environ["PYTHONSTARTUP"] = str(PACKAGE_DIR / "misc" / "shell_welcome_message.py")
  277. ################################################################################
  278. ### Internationalization & Localization Settings
  279. ################################################################################
  280. LANGUAGE_CODE = "en-us"
  281. USE_I18N = True
  282. USE_TZ = True
  283. DATETIME_FORMAT = "Y-m-d h:i:s A"
  284. SHORT_DATETIME_FORMAT = "Y-m-d h:i:s A"
  285. TIME_ZONE = CONSTANTS.TIMEZONE # django convention is TIME_ZONE, archivebox config uses TIMEZONE, they are equivalent
  286. from django.conf.locale.en import formats as en_formats # type: ignore
  287. en_formats.DATETIME_FORMAT = DATETIME_FORMAT # monkey patch en_format default with our preferred format
  288. en_formats.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT
  289. ################################################################################
  290. ### Logging Settings
  291. ################################################################################
  292. from .settings_logging import SETTINGS_LOGGING, LOGS_DIR, ERROR_LOG
  293. LOGGING = SETTINGS_LOGGING
  294. ################################################################################
  295. ### REST API Outbound Webhooks settings
  296. ################################################################################
  297. # Add default webhook configuration to the User model
  298. SIGNAL_WEBHOOKS_CUSTOM_MODEL = "archivebox.api.models.OutboundWebhook"
  299. SIGNAL_WEBHOOKS = {
  300. "HOOKS": {
  301. # ... is a special sigil value that means "use the default autogenerated hooks"
  302. "django.contrib.auth.models.User": ...,
  303. "archivebox.core.models.Snapshot": ...,
  304. "archivebox.core.models.ArchiveResult": ...,
  305. "archivebox.core.models.Tag": ...,
  306. "archivebox.api.models.APIToken": ...,
  307. },
  308. }
  309. ################################################################################
  310. ### Admin Data View Settings
  311. ################################################################################
  312. ADMIN_DATA_VIEWS = {
  313. "NAME": "Environment",
  314. "URLS": [
  315. {
  316. "route": "config/",
  317. "view": "archivebox.core.views.live_config_list_view",
  318. "name": "Configuration",
  319. "items": {
  320. "route": "<str:key>/",
  321. "view": "archivebox.core.views.live_config_value_view",
  322. "name": "config_val",
  323. },
  324. },
  325. {
  326. "route": "binaries/",
  327. "view": "archivebox.config.views.binaries_list_view",
  328. "name": "Dependencies",
  329. "items": {
  330. "route": "<str:key>/",
  331. "view": "archivebox.config.views.binary_detail_view",
  332. "name": "binary",
  333. },
  334. },
  335. {
  336. "route": "plugins/",
  337. "view": "archivebox.config.views.plugins_list_view",
  338. "name": "Plugins",
  339. "items": {
  340. "route": "<str:key>/",
  341. "view": "archivebox.config.views.plugin_detail_view",
  342. "name": "plugin",
  343. },
  344. },
  345. {
  346. "route": "workers/",
  347. "view": "archivebox.config.views.worker_list_view",
  348. "name": "Workers",
  349. "items": {
  350. "route": "<str:key>/",
  351. "view": "archivebox.config.views.worker_detail_view",
  352. "name": "worker",
  353. },
  354. },
  355. {
  356. "route": "logs/",
  357. "view": "archivebox.config.views.log_list_view",
  358. "name": "Logs",
  359. "items": {
  360. "route": "<str:key>/",
  361. "view": "archivebox.config.views.log_detail_view",
  362. "name": "log",
  363. },
  364. },
  365. # Additional admin data views from plugins
  366. ],
  367. }
  368. ################################################################################
  369. ### Debug Settings
  370. ################################################################################
  371. # only enable debug toolbar when in DEBUG mode with --nothreading (it doesnt work in multithreaded mode)
  372. DEBUG_TOOLBAR = False
  373. DEBUG_TOOLBAR = DEBUG_TOOLBAR and DEBUG and ("--nothreading" in sys.argv) and ("--reload" not in sys.argv)
  374. if DEBUG_TOOLBAR:
  375. try:
  376. import debug_toolbar # noqa
  377. DEBUG_TOOLBAR = True
  378. except ImportError:
  379. DEBUG_TOOLBAR = False
  380. if DEBUG_TOOLBAR:
  381. INSTALLED_APPS = [*INSTALLED_APPS, "debug_toolbar"]
  382. INTERNAL_IPS = ["0.0.0.0", "127.0.0.1", "*"]
  383. DEBUG_TOOLBAR_CONFIG = {
  384. "SHOW_TOOLBAR_CALLBACK": lambda request: True,
  385. "RENDER_PANELS": True,
  386. }
  387. DEBUG_TOOLBAR_PANELS = [
  388. "debug_toolbar.panels.history.HistoryPanel",
  389. "debug_toolbar.panels.versions.VersionsPanel",
  390. "debug_toolbar.panels.timer.TimerPanel",
  391. "debug_toolbar.panels.settings.SettingsPanel",
  392. "debug_toolbar.panels.headers.HeadersPanel",
  393. "debug_toolbar.panels.request.RequestPanel",
  394. "debug_toolbar.panels.sql.SQLPanel",
  395. "debug_toolbar.panels.staticfiles.StaticFilesPanel",
  396. # 'debug_toolbar.panels.templates.TemplatesPanel',
  397. "debug_toolbar.panels.cache.CachePanel",
  398. "debug_toolbar.panels.signals.SignalsPanel",
  399. "debug_toolbar.panels.logging.LoggingPanel",
  400. "debug_toolbar.panels.redirects.RedirectsPanel",
  401. "debug_toolbar.panels.profiling.ProfilingPanel",
  402. "djdt_flamegraph.FlamegraphPanel",
  403. ]
  404. MIDDLEWARE = [*MIDDLEWARE, "debug_toolbar.middleware.DebugToolbarMiddleware"]
  405. if DEBUG:
  406. from django_autotyping.typing import AutotypingSettingsDict
  407. INSTALLED_APPS += ["django_autotyping"]
  408. AUTOTYPING: AutotypingSettingsDict = {
  409. "STUBS_GENERATION": {
  410. "LOCAL_STUBS_DIR": PACKAGE_DIR / "typings",
  411. }
  412. }
  413. # https://github.com/bensi94/Django-Requests-Tracker (improved version of django-debug-toolbar)
  414. # Must delete archivebox/templates/admin to use because it relies on some things we override
  415. # visit /__requests_tracker__/ to access
  416. DEBUG_REQUESTS_TRACKER = True
  417. DEBUG_REQUESTS_TRACKER = DEBUG_REQUESTS_TRACKER and DEBUG
  418. if DEBUG_REQUESTS_TRACKER:
  419. import requests_tracker
  420. INSTALLED_APPS += ["requests_tracker"]
  421. MIDDLEWARE += ["requests_tracker.middleware.requests_tracker_middleware"]
  422. INTERNAL_IPS = ["127.0.0.1", "10.0.2.2", "0.0.0.0", "*"]
  423. TEMPLATE_DIRS.insert(0, str(Path(inspect.getfile(requests_tracker)).parent / "templates"))
  424. REQUESTS_TRACKER_CONFIG = {
  425. "TRACK_SQL": True,
  426. "ENABLE_STACKTRACES": False,
  427. "IGNORE_PATHS_PATTERNS": (
  428. r".*/favicon\.ico",
  429. r".*\.png",
  430. r"/admin/jsi18n/",
  431. ),
  432. "IGNORE_SQL_PATTERNS": (
  433. r"^SELECT .* FROM django_migrations WHERE app = 'requests_tracker'",
  434. r"^SELECT .* FROM django_migrations WHERE app = 'auth'",
  435. ),
  436. }
  437. # # https://docs.pydantic.dev/logfire/integrations/django/ (similar to DataDog / NewRelic / etc.)
  438. # DEBUG_LOGFIRE = False
  439. # DEBUG_LOGFIRE = DEBUG_LOGFIRE and os.access(DATA_DIR / '.logfire', os.W_OK) and (DATA_DIR / '.logfire').is_dir()
  440. # For usage with https://www.jetadmin.io/integrations/django
  441. # INSTALLED_APPS += ['jet_django']
  442. # JET_PROJECT = 'archivebox'
  443. # JET_TOKEN = 'some-api-token-here'
  444. # import ipdb; ipdb.set_trace()