Преглед на файлове

setup daphne and django channels to replace runserver

Nick Sweeting преди 1 година
родител
ревизия
00aa7dc19f
променени са 4 файла, в които са добавени 439 реда и са изтрити 103 реда
  1. 146 84
      archivebox/core/settings.py
  2. 3 3
      archivebox/main.py
  3. 289 16
      pdm.lock
  4. 1 0
      pyproject.toml

+ 146 - 84
archivebox/core/settings.py

@@ -78,6 +78,8 @@ DEBUG = CONFIG.DEBUG or ('--debug' in sys.argv)
 
 
 INSTALLED_APPS = [
+    'daphne',
+    
     # Django default apps
     'django.contrib.auth',
     'django.contrib.contenttypes',
@@ -351,38 +353,47 @@ en_formats.SHORT_DATETIME_FORMAT = SHORT_DATETIME_FORMAT
 ### Logging Settings
 ################################################################################
 
-IGNORABLE_404_URLS = [
-    re.compile(r'apple-touch-icon.*\.png$'),
-    re.compile(r'favicon\.ico$'),
-    re.compile(r'robots\.txt$'),
-    re.compile(r'.*\.(css|js)\.map$'),
-]
-IGNORABLE_200_URLS = [
-    re.compile(r'.*"GET /static/.* HTTP/.*" 2|3.+', re.I | re.M),
-    re.compile(r'.*"GET /admin/jsi18n/ HTTP/1.1" 200 .+', re.I | re.M),
+IGNORABLE_URL_PATTERNS = [
+    re.compile(r"/.*/?apple-touch-icon.*\.png"),
+    re.compile(r"/.*/?favicon\.ico"),
+    re.compile(r"/.*/?robots\.txt"),
+    re.compile(r"/.*/?.*\.(css|js)\.map"),
+    re.compile(r"/.*/?.*\.(css|js)\.map"),
+    re.compile(r"/static/.*"),
+    re.compile(r"/admin/jsi18n/"),
 ]
 
 class NoisyRequestsFilter(logging.Filter):
     def filter(self, record) -> bool:
         logline = record.getMessage()
-
-        # ignore harmless 404s for the patterns in IGNORABLE_404_URLS
-        for ignorable_url_pattern in IGNORABLE_404_URLS:
-            ignorable_log_pattern = re.compile(f'"GET /.*/?{ignorable_url_pattern.pattern[:-1]} HTTP/.*" (200|30.|404) .+$', re.I | re.M)
-            if ignorable_log_pattern.match(logline):
+        # '"GET /api/v1/docs HTTP/1.1" 200 1023'
+        # '"GET /static/admin/js/SelectFilter2.js HTTP/1.1" 200 15502'
+        # '"GET /static/admin/js/SelectBox.js HTTP/1.1" 304 0'
+        # '"GET /admin/jsi18n/ HTTP/1.1" 200 3352'
+        # '"GET /admin/api/apitoken/0191bbf8-fd5e-0b8c-83a8-0f32f048a0af/change/ HTTP/1.1" 200 28778'
+
+        # ignore harmless 404s for the patterns in IGNORABLE_URL_PATTERNS
+        for pattern in IGNORABLE_URL_PATTERNS:
+            ignorable_GET_request = re.compile(f'"GET {pattern.pattern} HTTP/.*" (2..|30.|404) .+$', re.I | re.M)
+            if ignorable_GET_request.match(logline):
                 return False
 
-            ignorable_log_pattern = re.compile(f'Not Found: /.*/?{ignorable_url_pattern.pattern}', re.I | re.M)
-            if ignorable_log_pattern.match(logline):
+            ignorable_404_pattern = re.compile(f'Not Found: {pattern.pattern}', re.I | re.M)
+            if ignorable_404_pattern.match(logline):
                 return False
 
-        # ignore staticfile requests that 200 or 30*
-        for ignorable_url_pattern in IGNORABLE_200_URLS:
-            if ignorable_log_pattern.match(logline):
-                return False
-            
         return True
 
+def add_extra_logging_attrs(record):
+    record.username = ''
+    try:
+        record.username = record.request.user.username
+    except AttributeError:
+        record.username = "Anonymous"
+        if hasattr(record, 'request'):
+            import ipdb; ipdb.set_trace()
+    return True
+
 
 ERROR_LOG = tempfile.NamedTemporaryFile().name
 
@@ -393,35 +404,38 @@ else:
     # if there's an issue on startup, we trash the log and let user figure it out via stdout/stderr
     print(f'[!] WARNING: data/logs dir does not exist. Logging to temp file: {ERROR_LOG}')
 
+
+LOG_LEVEL_DATABASE = 'DEBUG' if DEBUG else 'WARNING'
+LOG_LEVEL_REQUEST = 'DEBUG' if DEBUG else 'WARNING'
+
+import pydantic
+import django.template
+
 LOGGING = {
-    'version': 1,
-    'disable_existing_loggers': False,
-    'handlers': {
-        "console": {
-            "level": "DEBUG",
-            "filters": [],
-            'formatter': 'simple',
-            "class": "logging.StreamHandler",
-            'filters': ['noisyrequestsfilter'],
+    "version": 1,
+    "disable_existing_loggers": False,
+    "formatters": {
+        "rich": {
+            "datefmt": "[%X]",
+            # "format": "{asctime} {levelname} {module} {name} {message} {username}",
+            # "format": "%(message)s  (user=%(username)s",
         },
-        'logfile': {
-            'level': 'ERROR',
-            'class': 'logging.handlers.RotatingFileHandler',
-            'filename': ERROR_LOG,
-            'maxBytes': 1024 * 1024 * 25,  # 25 MB
-            'backupCount': 10,
-            'formatter': 'verbose',
-            'filters': ['noisyrequestsfilter'],
+        "verbose": {
+            "style": "{",
+        },
+        "simple": {
+            "format": "{name} {message}",
+            "style": "{",
+        },
+        "django.server": {
+            "()": "django.utils.log.ServerFormatter",
+            # "format": "{message} (user={username})",
+            "style": "{",
         },
-        # "mail_admins": {
-        #     "level": "ERROR",
-        #     "filters": ["require_debug_false"],
-        #     "class": "django.utils.log.AdminEmailHandler",
-        # },
     },
-    'filters': {
-        'noisyrequestsfilter': {
-            '()': NoisyRequestsFilter,
+    "filters": {
+        "noisyrequestsfilter": {
+            "()": NoisyRequestsFilter,
         },
         "require_debug_false": {
             "()": "django.utils.log.RequireDebugFalse",
@@ -429,58 +443,106 @@ LOGGING = {
         "require_debug_true": {
             "()": "django.utils.log.RequireDebugTrue",
         },
+        # "add_extra_logging_attrs": {
+        #     "()": "django.utils.log.CallbackFilter",
+        #     "callback": add_extra_logging_attrs,
+        # },
     },
-    'formatters': {
-        'verbose': {
-            'format': '{name} {levelname} {asctime} {module} {process:d} {thread:d} {message}',
-            'style': '{',
+    "handlers": {
+        # "console": {
+        #     "level": "DEBUG",
+        #     'formatter': 'simple',
+        #     "class": "logging.StreamHandler",
+        #     'filters': ['noisyrequestsfilter', 'add_extra_logging_attrs'],
+        # },
+        "console": {
+            "class": "rich.logging.RichHandler",
+            "formatter": "rich",
+            "level": "DEBUG",
+            "markup": False,
+            "rich_tracebacks": True,
+            "filters": ["noisyrequestsfilter"],
+            "tracebacks_suppress": [
+                pydantic,
+                django.template,
+            ],
         },
-        'simple': {
-            'format': '{name} {message}',
-            'style': '{',
+        "logfile": {
+            "level": "ERROR",
+            "class": "logging.handlers.RotatingFileHandler",
+            "filename": ERROR_LOG,
+            "maxBytes": 1024 * 1024 * 25,  # 25 MB
+            "backupCount": 10,
+            "formatter": "verbose",
+            "filters": ["noisyrequestsfilter"],
         },
-        "django.server": {
-            "()": "django.utils.log.ServerFormatter",
-            "format": "[{server_time}] {message}",
-            "style": "{",
+        # "mail_admins": {
+        #     "level": "ERROR",
+        #     "filters": ["require_debug_false"],
+        #     "class": "django.utils.log.AdminEmailHandler",
+        # },
+        "null": {
+            "class": "logging.NullHandler",
         },
     },
-    'loggers': {
-        'api': {
-            'handlers': ['console', 'logfile'],
-            'level': 'DEBUG',
+    "root": {
+        "handlers": ["console", "logfile"],
+        "level": "INFO",
+        "formatter": "verbose",
+    },
+    "loggers": {
+        "api": {
+            "handlers": ["console", "logfile"],
+            "level": "DEBUG",
         },
-        'checks': {
-            'handlers': ['console', 'logfile'],
-            'level': 'DEBUG',
+        "checks": {
+            "handlers": ["console", "logfile"],
+            "level": "DEBUG",
         },
-        'core': {
-            'handlers': ['console', 'logfile'],
-            'level': 'DEBUG',
+        "core": {
+            "handlers": ["console", "logfile"],
+            "level": "DEBUG",
         },
-        'builtin_plugins': {
-            'handlers': ['console', 'logfile'],
-            'level': 'DEBUG',
+        "builtin_plugins": {
+            "handlers": ["console", "logfile"],
+            "level": "DEBUG",
+        },
+        "django": {
+            "handlers": ["console", "logfile"],
+            "level": "INFO",
+            "filters": ["noisyrequestsfilter"],
         },
-        'django': {
-            'handlers': ['console', 'logfile'],
-            'level': 'INFO',
-            'filters': ['noisyrequestsfilter'],
+        "django.utils.autoreload": {
+            "propagate": False,
+            "handlers": [],
+            "level": "ERROR",
         },
-        'django.server': {
-            'handlers': ['console', 'logfile'],
-            'level': 'INFO',
-            'filters': ['noisyrequestsfilter'],
-            'propagate': False,
+        "django.channels.server": {
+            "propagate": False,
+            "handlers": ["console", "logfile"],
+            "level": "INFO",
+            "filters": ["noisyrequestsfilter"],
             "formatter": "django.server",
         },
-        'django.request': {
-            'handlers': ['console', 'logfile'],
-            'level': 'INFO',
-            'filters': ['noisyrequestsfilter'],
-            'propagate': False,
+        "django.server": {  # logs all requests (2xx, 3xx, 4xx)
+            "propagate": False,
+            "handlers": ["console", "logfile"],
+            "level": "INFO",
+            "filters": ["noisyrequestsfilter"],
             "formatter": "django.server",
         },
+        "django.request": {  # only logs 4xx and 5xx errors
+            "propagate": False,
+            "handlers": ["console", "logfile"],
+            "level": "INFO",
+            "filters": ["noisyrequestsfilter"],
+            "formatter": "django.server",
+        },
+        "django.db.backends": {
+            "propagate": False,
+            "handlers": ["console"],
+            "level": LOG_LEVEL_DATABASE,
+        },
     },
 }
 

+ 3 - 3
archivebox/main.py

@@ -1335,9 +1335,9 @@ def server(runserver_args: Optional[List[str]]=None,
         print('        archivebox manage createsuperuser')
         print()
 
-    # fallback to serving staticfiles insecurely with django when DEBUG=False
-    if not config.DEBUG:
-        runserver_args.append('--insecure')  # TODO: serve statics w/ nginx instead
+    # fallback to serving staticfiles insecurely with django when DEBUG=False (not compatible with daphne)
+    # if not config.DEBUG:
+    #     runserver_args.append('--insecure')  # TODO: serve statics w/ nginx instead
     
     # toggle autoreloading when archivebox code changes (it's on by default)
     if not reload:

+ 289 - 16
pdm.lock

@@ -5,7 +5,7 @@
 groups = ["default", "ldap", "sonic"]
 strategy = ["inherit_metadata"]
 lock_version = "4.5.0"
-content_hash = "sha256:c6aa1f436032d18d079a4c2e9d9b95a5110579eb96a449751bfaf4d472eba401"
+content_hash = "sha256:f940c4c0a330b7b0bcff68a006b29ea3b1292ad6aadd3cfc909de0622f2963ac"
 
 [[metadata.targets]]
 requires_python = "==3.10.*"
@@ -90,6 +90,54 @@ files = [
     {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
 ]
 
+[[package]]
+name = "attrs"
+version = "24.2.0"
+requires_python = ">=3.7"
+summary = "Classes Without Boilerplate"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "importlib-metadata; python_version < \"3.8\"",
+]
+files = [
+    {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
+    {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
+]
+
+[[package]]
+name = "autobahn"
+version = "24.4.2"
+requires_python = ">=3.9"
+summary = "WebSocket client & server library, WAMP real-time framework"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "cryptography>=3.4.6",
+    "hyperlink>=21.0.0",
+    "setuptools",
+    "txaio>=21.2.1",
+]
+files = [
+    {file = "autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81"},
+    {file = "autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9"},
+]
+
+[[package]]
+name = "automat"
+version = "24.8.1"
+requires_python = ">=3.8"
+summary = "Self-service finite-state machines for the programmer on the go."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "typing-extensions; python_version < \"3.10\"",
+]
+files = [
+    {file = "Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a"},
+    {file = "automat-24.8.1.tar.gz", hash = "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88"},
+]
+
 [[package]]
 name = "base32-crockford"
 version = "0.3.0"
@@ -157,6 +205,39 @@ files = [
     {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
 ]
 
+[[package]]
+name = "channels"
+version = "4.1.0"
+requires_python = ">=3.8"
+summary = "Brings async, event-driven capabilities to Django 3.2 and up."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "Django>=4.2",
+    "asgiref<4,>=3.6.0",
+]
+files = [
+    {file = "channels-4.1.0-py3-none-any.whl", hash = "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48"},
+    {file = "channels-4.1.0.tar.gz", hash = "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"},
+]
+
+[[package]]
+name = "channels"
+version = "4.1.0"
+extras = ["daphne"]
+requires_python = ">=3.8"
+summary = "Brings async, event-driven capabilities to Django 3.2 and up."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "channels==4.1.0",
+    "daphne>=4.0.0",
+]
+files = [
+    {file = "channels-4.1.0-py3-none-any.whl", hash = "sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48"},
+    {file = "channels-4.1.0.tar.gz", hash = "sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d"},
+]
+
 [[package]]
 name = "charset-normalizer"
 version = "3.3.2"
@@ -172,6 +253,18 @@ files = [
     {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
 ]
 
+[[package]]
+name = "constantly"
+version = "23.10.4"
+requires_python = ">=3.8"
+summary = "Symbolic constants in Python"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+files = [
+    {file = "constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9"},
+    {file = "constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd"},
+]
+
 [[package]]
 name = "croniter"
 version = "3.0.3"
@@ -206,6 +299,23 @@ files = [
     {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
 ]
 
+[[package]]
+name = "daphne"
+version = "4.1.2"
+requires_python = ">=3.8"
+summary = "Django ASGI (HTTP/WebSocket) server"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "asgiref<4,>=3.5.2",
+    "autobahn>=22.4.2",
+    "twisted[tls]>=22.4",
+]
+files = [
+    {file = "daphne-4.1.2-py3-none-any.whl", hash = "sha256:618d1322bb4d875342b99dd2a10da2d9aae7ee3645f765965fdc1e658ea5290a"},
+    {file = "daphne-4.1.2.tar.gz", hash = "sha256:fcbcace38eb86624ae247c7ffdc8ac12f155d7d19eafac4247381896d6f33761"},
+]
+
 [[package]]
 name = "dateparser"
 version = "1.2.0"
@@ -534,6 +644,22 @@ files = [
     {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
 ]
 
+[[package]]
+name = "hyperlink"
+version = "21.0.0"
+requires_python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+summary = "A featureful, immutable, and correct URL for Python."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "idna>=2.5",
+    "typing; python_version < \"3.5\"",
+]
+files = [
+    {file = "hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4"},
+    {file = "hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b"},
+]
+
 [[package]]
 name = "idna"
 version = "3.8"
@@ -546,6 +672,22 @@ files = [
     {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"},
 ]
 
+[[package]]
+name = "incremental"
+version = "24.7.2"
+requires_python = ">=3.8"
+summary = "A small library that versions your Python projects."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "setuptools>=61.0",
+    "tomli; python_version < \"3.11\"",
+]
+files = [
+    {file = "incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe"},
+    {file = "incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9"},
+]
+
 [[package]]
 name = "ipython"
 version = "8.26.0"
@@ -586,6 +728,21 @@ files = [
     {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"},
 ]
 
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+requires_python = ">=3.8"
+summary = "Python port of markdown-it. Markdown parsing, done right!"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "mdurl~=0.1",
+]
+files = [
+    {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+    {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
 [[package]]
 name = "matplotlib-inline"
 version = "0.1.7"
@@ -601,6 +758,18 @@ files = [
     {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
 ]
 
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+requires_python = ">=3.7"
+summary = "Markdown URL utilities"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+files = [
+    {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+    {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
 [[package]]
 name = "mutagen"
 version = "1.47.0"
@@ -664,19 +833,6 @@ dependencies = [
     "requests",
 ]
 
-[[package]]
-name = "pocket"
-version = "0.3.7"
-git = "https://github.com/tapanpandita/pocket.git"
-ref = "v0.3.7"
-revision = "5a144438cc89bfc0ec94db960718ccf1f76468c1"
-summary = "api wrapper for getpocket.com"
-groups = ["default"]
-marker = "python_version == \"3.10\""
-dependencies = [
-    "requests",
-]
-
 [[package]]
 name = "prompt-toolkit"
 version = "3.0.47"
@@ -719,7 +875,7 @@ name = "pyasn1"
 version = "0.6.0"
 requires_python = ">=3.8"
 summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
-groups = ["ldap"]
+groups = ["default", "ldap"]
 marker = "python_version == \"3.10\""
 files = [
     {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
@@ -731,7 +887,7 @@ name = "pyasn1-modules"
 version = "0.4.0"
 requires_python = ">=3.8"
 summary = "A collection of ASN.1-based protocols modules"
-groups = ["ldap"]
+groups = ["default", "ldap"]
 marker = "python_version == \"3.10\""
 dependencies = [
     "pyasn1<0.7.0,>=0.4.6",
@@ -832,6 +988,21 @@ files = [
     {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
 ]
 
+[[package]]
+name = "pyopenssl"
+version = "24.2.1"
+requires_python = ">=3.7"
+summary = "Python wrapper module around the OpenSSL library"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "cryptography<44,>=41.0.5",
+]
+files = [
+    {file = "pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d"},
+    {file = "pyopenssl-24.2.1.tar.gz", hash = "sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95"},
+]
+
 [[package]]
 name = "python-crontab"
 version = "3.2.0"
@@ -920,6 +1091,41 @@ files = [
     {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
 ]
 
+[[package]]
+name = "rich"
+version = "13.8.0"
+requires_python = ">=3.7.0"
+summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "markdown-it-py>=2.2.0",
+    "pygments<3.0.0,>=2.13.0",
+    "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"",
+]
+files = [
+    {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
+    {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
+]
+
+[[package]]
+name = "service-identity"
+version = "24.1.0"
+requires_python = ">=3.8"
+summary = "Service identity verification for pyOpenSSL & cryptography."
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "attrs>=19.1.0",
+    "cryptography",
+    "pyasn1",
+    "pyasn1-modules",
+]
+files = [
+    {file = "service_identity-24.1.0-py3-none-any.whl", hash = "sha256:a28caf8130c8a5c1c7a6f5293faaf239bbfb7751e4862436920ee6f2616f568a"},
+    {file = "service_identity-24.1.0.tar.gz", hash = "sha256:6829c9d62fb832c2e1c435629b0a8c476e1929881f28bee4d20bc24161009221"},
+]
+
 [[package]]
 name = "setuptools"
 version = "74.0.0"
@@ -1029,6 +1235,58 @@ files = [
     {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
 ]
 
+[[package]]
+name = "twisted"
+version = "24.7.0"
+requires_python = ">=3.8.0"
+summary = "An asynchronous networking framework written in Python"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "attrs>=21.3.0",
+    "automat>=0.8.0",
+    "constantly>=15.1",
+    "hyperlink>=17.1.1",
+    "incremental>=24.7.0",
+    "typing-extensions>=4.2.0",
+    "zope-interface>=5",
+]
+files = [
+    {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"},
+    {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"},
+]
+
+[[package]]
+name = "twisted"
+version = "24.7.0"
+extras = ["tls"]
+requires_python = ">=3.8.0"
+summary = "An asynchronous networking framework written in Python"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "idna>=2.4",
+    "pyopenssl>=21.0.0",
+    "service-identity>=18.1.0",
+    "twisted==24.7.0",
+]
+files = [
+    {file = "twisted-24.7.0-py3-none-any.whl", hash = "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81"},
+    {file = "twisted-24.7.0.tar.gz", hash = "sha256:5a60147f044187a127ec7da96d170d49bcce50c6fd36f594e60f4587eff4d394"},
+]
+
+[[package]]
+name = "txaio"
+version = "23.1.1"
+requires_python = ">=3.7"
+summary = "Compatibility API between asyncio/Twisted/Trollius"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+files = [
+    {file = "txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490"},
+    {file = "txaio-23.1.1.tar.gz", hash = "sha256:f9a9216e976e5e3246dfd112ad7ad55ca915606b60b84a757ac769bd404ff704"},
+]
+
 [[package]]
 name = "typeid-python"
 version = "0.3.1"
@@ -1183,3 +1441,18 @@ files = [
     {file = "yt_dlp-2024.8.6-py3-none-any.whl", hash = "sha256:ab507ff600bd9269ad4d654e309646976778f0e243eaa2f6c3c3214278bb2922"},
     {file = "yt_dlp-2024.8.6.tar.gz", hash = "sha256:e8551f26bc8bf67b99c12373cc87ed2073436c3437e53290878d0f4b4bb1f663"},
 ]
+
+[[package]]
+name = "zope-interface"
+version = "7.0.3"
+requires_python = ">=3.8"
+summary = "Interfaces for Python"
+groups = ["default"]
+marker = "python_version == \"3.10\""
+dependencies = [
+    "setuptools",
+]
+files = [
+    {file = "zope.interface-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58"},
+    {file = "zope.interface-7.0.3.tar.gz", hash = "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1"},
+]

+ 1 - 0
pyproject.toml

@@ -49,6 +49,7 @@ dependencies = [
     "django-taggit==1.3.0",
     "base32-crockford==0.3.0",
     "rich>=13.8.0",
+    "channels[daphne]>=4.1.0",
 ]
 
 homepage = "https://github.com/ArchiveBox/ArchiveBox"