diff --git a/archivebox/abx/__init__.py b/archivebox/abx/__init__.py
deleted file mode 100644
index c571a2e3..00000000
--- a/archivebox/abx/__init__.py
+++ /dev/null
@@ -1,131 +0,0 @@
-__package__ = 'abx'
-
-import importlib
-from pathlib import Path
-from typing import Dict, Callable, List
-
-from . import hookspec as base_spec
-from abx.hookspec import hookimpl, hookspec           # noqa
-from abx.manager import pm, PluginManager             # noqa
-
-
-pm.add_hookspecs(base_spec)
-
-
-###### PLUGIN DISCOVERY AND LOADING ########################################################
-
-def get_plugin_order(plugin_entrypoint: Path):
-    order = 999
-    try:
-        # if .plugin_order file exists, use it to set the load priority
-        order = int((plugin_entrypoint.parent / '.plugin_order').read_text())
-    except FileNotFoundError:
-        pass
-    return (order, plugin_entrypoint)
-
-def register_hookspecs(hookspecs: List[str]):
-    """
-    Register all the hookspecs from a list of module names.
-    """
-    for hookspec_import_path in hookspecs:
-        hookspec_module = importlib.import_module(hookspec_import_path)
-        pm.add_hookspecs(hookspec_module)
-
-
-def find_plugins_in_dir(plugins_dir: Path, prefix: str) -> Dict[str, Path]:
-    """
-    Find all the plugins in a given directory. Just looks for an __init__.py file.
-    """
-    return {
-        f"{prefix}.{plugin_entrypoint.parent.name}": plugin_entrypoint.parent
-        for plugin_entrypoint in sorted(plugins_dir.glob("*/__init__.py"), key=get_plugin_order)
-        if plugin_entrypoint.parent.name != 'abx'
-    }   # "plugins_pkg.pip": "/app/archivebox/plugins_pkg/pip"
-
-
-def get_pip_installed_plugins(group='abx'):
-    """replaces pm.load_setuptools_entrypoints("abx"), finds plugins that registered entrypoints via pip"""
-    import importlib.metadata
-
-    DETECTED_PLUGINS = {}   # module_name: module_dir_path
-    for dist in list(importlib.metadata.distributions()):
-        for entrypoint in dist.entry_points:
-            if entrypoint.group != group or pm.is_blocked(entrypoint.name):
-                continue
-            DETECTED_PLUGINS[entrypoint.name] = Path(entrypoint.load().__file__).parent
-            # pm.register(plugin, name=ep.name)
-            # pm._plugin_distinfo.append((plugin, DistFacade(dist)))
-    return DETECTED_PLUGINS
-
-
-def get_plugins_in_dirs(plugin_dirs: Dict[str, Path]):
-    """
-    Get the mapping of dir_name: {plugin_id: plugin_dir} for all plugins in the given directories.
-    """
-    DETECTED_PLUGINS = {}
-    for plugin_prefix, plugin_dir in plugin_dirs.items():
-        DETECTED_PLUGINS.update(find_plugins_in_dir(plugin_dir, prefix=plugin_prefix))
-    return DETECTED_PLUGINS
-
-
-# Load all plugins from pip packages, archivebox built-ins, and user plugins
-
-def load_plugins(plugins_dict: Dict[str, Path]):
-    """
-    Load all the plugins from a dictionary of module names and directory paths.
-    """
-    LOADED_PLUGINS = {}
-    for plugin_module, plugin_dir in plugins_dict.items():
-        # print(f'Loading plugin: {plugin_module} from {plugin_dir}')
-        plugin_module_loaded = importlib.import_module(plugin_module)
-        pm.register(plugin_module_loaded)
-        LOADED_PLUGINS[plugin_module] = plugin_module_loaded.PLUGIN
-        # print(f'    √ Loaded plugin: {plugin_module}')
-    return LOADED_PLUGINS
-
-def get_registered_plugins():
-    """
-    Get all the plugins registered with Pluggy.
-    """
-    plugins = {}
-    plugin_to_distinfo = dict(pm.list_plugin_distinfo())
-    for plugin in pm.get_plugins():
-        plugin_info = {
-            "name": plugin.__name__,
-            "hooks": [h.name for h in pm.get_hookcallers(plugin) or ()],
-        }
-        distinfo = plugin_to_distinfo.get(plugin)
-        if distinfo:
-            plugin_info["version"] = distinfo.version
-            plugin_info["name"] = (
-                getattr(distinfo, "name", None) or distinfo.project_name
-            )
-        plugins[plugin_info["name"]] = plugin_info
-    return plugins
-
-
-
-
-def get_plugin_hooks(plugin_pkg: str | None) -> Dict[str, Callable]:
-    """
-    Get all the functions marked with @hookimpl on a module.
-    """
-    if not plugin_pkg:
-        return {}
-    
-    hooks = {}
-    
-    plugin_module = importlib.import_module(plugin_pkg)
-    for attr_name in dir(plugin_module):
-        if attr_name.startswith('_'):
-            continue
-        try:
-            attr = getattr(plugin_module, attr_name)
-            if isinstance(attr, Callable):
-                hooks[attr_name] = None
-                pm.parse_hookimpl_opts(plugin_module, attr_name)
-                hooks[attr_name] = attr
-        except Exception as e:
-            print(f'Error getting hookimpls for {plugin_pkg}: {e}')
-
-    return hooks
diff --git a/archivebox/abx/archivebox/__init__.py b/archivebox/abx/archivebox/__init__.py
deleted file mode 100644
index 58bbb447..00000000
--- a/archivebox/abx/archivebox/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-__package__ = 'abx.archivebox'
-
-import os
-import importlib
-
-from typing import Dict
-from pathlib import Path
-
-
-def load_archivebox_plugins(pm, plugins_dict: Dict[str, Path]):
-    """Load archivebox plugins, very similar to abx.load_plugins but it looks for a pydantic PLUGIN model + hooks in apps.py"""
-    LOADED_PLUGINS = {}
-    for plugin_module, plugin_dir in reversed(plugins_dict.items()):
-        # print(f'Loading plugin: {plugin_module} from {plugin_dir}')
-        
-        # 1. register the plugin module directly in case it contains any look hookimpls (e.g. in __init__.py)
-        try:
-            plugin_module_loaded = importlib.import_module(plugin_module)
-            pm.register(plugin_module_loaded)
-        except Exception as e:
-            print(f'Error registering plugin: {plugin_module} - {e}')
-            
-        
-        # 2. then try to import plugin_module.apps as well
-        if os.access(plugin_dir / 'apps.py', os.R_OK):
-            plugin_apps = importlib.import_module(plugin_module + '.apps')
-            pm.register(plugin_apps)                                           # register the whole .apps  in case it contains loose hookimpls (not in a class)
-            
-        # print(f'    √ Loaded plugin: {plugin_module} {len(archivebox_plugins_found) * "🧩"}')
-    return LOADED_PLUGINS
diff --git a/archivebox/abx/archivebox/base_binary.py b/archivebox/abx/archivebox/base_binary.py
deleted file mode 100644
index ee7ab5e1..00000000
--- a/archivebox/abx/archivebox/base_binary.py
+++ /dev/null
@@ -1,117 +0,0 @@
-__package__ = "abx.archivebox"
-
-import os
-from typing import Optional, cast
-from typing_extensions import Self
-
-from pydantic import validate_call
-from pydantic_pkgr import (
-    Binary,
-    BinProvider,
-    BinProviderName,
-    AptProvider,
-    BrewProvider,
-    EnvProvider,
-)
-
-from archivebox.config.permissions import ARCHIVEBOX_USER
-
-import abx
-
-
-class BaseBinProvider(BinProvider):
-    
-    # TODO: add install/load/load_or_install methods as abx.hookimpl methods
-    
-    @property
-    def admin_url(self) -> str:
-        # e.g. /admin/environment/binproviders/NpmBinProvider/   TODO
-        return "/admin/environment/binaries/"
-
-    @abx.hookimpl
-    def get_BINPROVIDERS(self):
-        return [self]
-
-class BaseBinary(Binary):
-    # TODO: formalize state diagram, final states, transitions, side effects, etc.
-
-    @staticmethod
-    def symlink_to_lib(binary, bin_dir=None) -> None:
-        from archivebox.config.common import STORAGE_CONFIG
-        bin_dir = bin_dir or STORAGE_CONFIG.LIB_DIR / 'bin'
-        
-        if not (binary.abspath and os.access(binary.abspath, os.R_OK)):
-            return
-        
-        try:
-            bin_dir.mkdir(parents=True, exist_ok=True)
-            symlink = bin_dir / binary.name
-            symlink.unlink(missing_ok=True)
-            symlink.symlink_to(binary.abspath)
-            symlink.chmod(0o777)   # make sure its executable by everyone
-        except Exception as err:
-            # print(f'[red]:warning: Failed to symlink {symlink} -> {binary.abspath}[/red] {err}')
-            # not actually needed, we can just run without it
-            pass
-        
-    @validate_call
-    def load(self, fresh=False, **kwargs) -> Self:
-        from archivebox.config.common import STORAGE_CONFIG
-        if fresh:
-            binary = super().load(**kwargs)
-            self.symlink_to_lib(binary=binary, bin_dir=STORAGE_CONFIG.LIB_DIR / 'bin')
-        else:
-            # get cached binary from db
-            try:
-                from machine.models import InstalledBinary
-                installed_binary = InstalledBinary.objects.get_from_db_or_cache(self)    # type: ignore
-                binary = InstalledBinary.load_from_db(installed_binary)
-            except Exception:
-                # maybe we are not in a DATA dir so there is no db, fallback to reading from fs
-                # (e.g. when archivebox version is run outside of a DATA dir)
-                binary = super().load(**kwargs)
-        return cast(Self, binary)
-    
-    @validate_call
-    def install(self, **kwargs) -> Self:
-        from archivebox.config.common import STORAGE_CONFIG
-        binary = super().install(**kwargs)
-        self.symlink_to_lib(binary=binary, bin_dir=STORAGE_CONFIG.LIB_DIR / 'bin')
-        return binary
-    
-    @validate_call
-    def load_or_install(self, fresh=False, **kwargs) -> Self:
-        from archivebox.config.common import STORAGE_CONFIG
-        try:
-            binary = self.load(fresh=fresh)
-            if binary and binary.version:
-                self.symlink_to_lib(binary=binary, bin_dir=STORAGE_CONFIG.LIB_DIR / 'bin')
-                return binary
-        except Exception:
-            pass
-        return self.install(**kwargs)
-    
-    @property
-    def admin_url(self) -> str:
-        # e.g. /admin/environment/config/LdapConfig/
-        return f"/admin/environment/binaries/{self.name}/"
-
-    @abx.hookimpl
-    def get_BINARIES(self):
-        return [self]
-
-
-class AptBinProvider(AptProvider, BaseBinProvider):
-    name: BinProviderName = "apt"
-    
-class BrewBinProvider(BrewProvider, BaseBinProvider):
-    name: BinProviderName = "brew"
-    
-class EnvBinProvider(EnvProvider, BaseBinProvider):
-    name: BinProviderName = "env"
-    
-    euid: Optional[int] = ARCHIVEBOX_USER
-
-apt = AptBinProvider()
-brew = BrewBinProvider()
-env = EnvBinProvider()
diff --git a/archivebox/abx/archivebox/base_extractor.py b/archivebox/abx/archivebox/base_extractor.py
deleted file mode 100644
index 51dcc8d2..00000000
--- a/archivebox/abx/archivebox/base_extractor.py
+++ /dev/null
@@ -1,204 +0,0 @@
-__package__ = 'abx.archivebox'
-
-import json
-import os
-
-from typing import Optional, List, Literal, Annotated, Dict, Any, Tuple
-from pathlib import Path
-
-from pydantic import AfterValidator
-from pydantic_pkgr import BinName
-from django.utils.functional import cached_property
-from django.utils import timezone
-
-import abx
-
-from .base_binary import BaseBinary
-
-
-def assert_no_empty_args(args: List[str]) -> List[str]:
-    assert all(len(arg) for arg in args)
-    return args
-
-ExtractorName = Annotated[str, AfterValidator(lambda s: s.isidentifier())]
-
-HandlerFuncStr = Annotated[str, AfterValidator(lambda s: s.startswith('self.'))]
-CmdArgsList = Annotated[List[str] | Tuple[str, ...], AfterValidator(assert_no_empty_args)]
-
-
-class BaseExtractor:
-    name: ExtractorName
-    binary: BinName
-
-    default_args: CmdArgsList = []
-    extra_args: CmdArgsList = []
-
-    def get_output_path(self, snapshot) -> Path:
-        return Path(self.__class__.__name__.lower())
-
-    def should_extract(self, uri: str, config: dict | None=None) -> bool:
-        try:
-            assert self.detect_installed_binary().version
-        except Exception:
-            raise
-            # could not load binary
-            return False
-        
-        # output_dir = self.get_output_path(snapshot)
-        # if output_dir.glob('*.*'):
-        #     return False
-        return True
-
-    @abx.hookimpl
-    def extract(self, snapshot_id: str) -> Dict[str, Any]:
-        from core.models import Snapshot
-        from archivebox import CONSTANTS
-        
-        snapshot = Snapshot.objects.get(id=snapshot_id)
-        
-        if not self.should_extract(snapshot.url):
-            return {}
-        
-        status = 'failed'
-        start_ts = timezone.now()
-        uplink = self.detect_network_interface()
-        installed_binary = self.detect_installed_binary()
-        machine = installed_binary.machine
-        assert uplink.machine == installed_binary.machine  # it would be *very* weird if this wasn't true
-        
-        output_dir = CONSTANTS.DATA_DIR / '.tmp' / 'extractors' / self.name / str(snapshot.abid)
-        output_dir.mkdir(parents=True, exist_ok=True)
-
-        # execute the extractor binary with the given args
-        args = [snapshot.url, *self.args] if self.args is not None else [snapshot.url, *self.default_args, *self.extra_args]
-        cmd = [str(installed_binary.abspath), *args]
-        proc = self.exec(installed_binary=installed_binary, args=args, cwd=output_dir)
-
-        # collect the output
-        end_ts = timezone.now()
-        output_files = list(str(path.relative_to(output_dir)) for path in output_dir.glob('**/*.*'))
-        stdout = proc.stdout.strip()
-        stderr = proc.stderr.strip()
-        output_json = None
-        output_text = stdout
-        try:
-            output_json = json.loads(stdout.strip())
-            output_text = None
-        except json.JSONDecodeError:
-            pass
-        
-        errors = []
-        if proc.returncode == 0:
-            status = 'success'
-        else:
-            errors.append(f'{installed_binary.name} returned non-zero exit code: {proc.returncode}')   
-
-        # increment health stats counters
-        if status == 'success':
-            machine.record_health_success()
-            uplink.record_health_success()
-            installed_binary.record_health_success()
-        else:
-            machine.record_health_failure()
-            uplink.record_health_failure()
-            installed_binary.record_health_failure()
-
-        return {
-            'extractor': self.name,
-            
-            'snapshot': {
-                'id': snapshot.id,
-                'abid': snapshot.abid,
-                'url': snapshot.url,
-                'created_by_id': snapshot.created_by_id,
-            },
-            
-            'machine': {
-                'id': machine.id,
-                'abid': machine.abid,
-                'guid': machine.guid,
-                'hostname': machine.hostname,
-                'hw_in_docker': machine.hw_in_docker,
-                'hw_in_vm': machine.hw_in_vm,
-                'hw_manufacturer': machine.hw_manufacturer,
-                'hw_product': machine.hw_product,
-                'hw_uuid': machine.hw_uuid,
-                'os_arch': machine.os_arch,
-                'os_family': machine.os_family,
-                'os_platform': machine.os_platform,
-                'os_release': machine.os_release,
-                'os_kernel': machine.os_kernel,
-            },
-            
-            'uplink': { 
-                'id': uplink.id,
-                'abid': uplink.abid,
-                'mac_address': uplink.mac_address,
-                'ip_public': uplink.ip_public,
-                'ip_local': uplink.ip_local,
-                'dns_server': uplink.dns_server,
-                'hostname': uplink.hostname,
-                'iface': uplink.iface,
-                'isp': uplink.isp,
-                'city': uplink.city,
-                'region': uplink.region,
-                'country': uplink.country,
-            },
-            
-            'binary': {
-                'id': installed_binary.id,
-                'abid': installed_binary.abid,
-                'name': installed_binary.name,
-                'binprovider': installed_binary.binprovider,
-                'abspath': installed_binary.abspath,
-                'version': installed_binary.version,
-                'sha256': installed_binary.sha256,
-            },
-
-            'cmd': cmd,
-            'stdout': stdout,
-            'stderr': stderr,
-            'returncode': proc.returncode,
-            'start_ts': start_ts,
-            'end_ts': end_ts,
-            
-            'status': status,
-            'errors': errors,
-            'output_dir': str(output_dir.relative_to(CONSTANTS.DATA_DIR)),
-            'output_files': output_files,
-            'output_json': output_json or {},
-            'output_text': output_text or '',
-        }
-
-    # TODO: move this to a hookimpl
-    def exec(self, args: CmdArgsList=(), cwd: Optional[Path]=None, installed_binary=None):
-        cwd = cwd or Path(os.getcwd())
-        binary = self.load_binary(installed_binary=installed_binary)
-        
-        return binary.exec(cmd=args, cwd=cwd)
-    
-    @cached_property
-    def BINARY(self) -> BaseBinary:
-        import abx.archivebox.reads
-        for binary in abx.archivebox.reads.get_BINARIES().values():
-            if binary.name == self.binary:
-                return binary
-        raise ValueError(f'Binary {self.binary} not found')
-    
-    def detect_installed_binary(self):
-        from machine.models import InstalledBinary
-        # hydrates binary from DB/cache if record of installed version is recent enough
-        # otherwise it finds it from scratch by detecting installed version/abspath/sha256 on host
-        return InstalledBinary.objects.get_from_db_or_cache(self.BINARY)
-
-    def load_binary(self, installed_binary=None) -> BaseBinary:
-        installed_binary = installed_binary or self.detect_installed_binary()
-        return installed_binary.load_from_db()
-    
-    def detect_network_interface(self):
-        from machine.models import NetworkInterface
-        return NetworkInterface.objects.current()
-
-    @abx.hookimpl
-    def get_EXTRACTORS(self):
-        return [self]
diff --git a/archivebox/abx/archivebox/base_replayer.py b/archivebox/abx/archivebox/base_replayer.py
deleted file mode 100644
index 097a9e94..00000000
--- a/archivebox/abx/archivebox/base_replayer.py
+++ /dev/null
@@ -1,25 +0,0 @@
-__package__ = 'abx.archivebox'
-
-import abx
-
-
-class BaseReplayer:
-    """Describes how to render an ArchiveResult in several contexts"""
-    
-    url_pattern: str = '*'
-
-    row_template: str = 'plugins/generic_replayer/templates/row.html'
-    embed_template: str = 'plugins/generic_replayer/templates/embed.html'
-    fullpage_template: str = 'plugins/generic_replayer/templates/fullpage.html'
-
-    # row_view: LazyImportStr = 'plugins.generic_replayer.views.row_view'
-    # embed_view: LazyImportStr = 'plugins.generic_replayer.views.embed_view'
-    # fullpage_view: LazyImportStr = 'plugins.generic_replayer.views.fullpage_view'
-    # icon_view: LazyImportStr = 'plugins.generic_replayer.views.get_icon'
-    # thumbnail_view: LazyImportStr = 'plugins.generic_replayer.views.get_icon'
-
-    @abx.hookimpl
-    def get_REPLAYERS(self):
-        return [self]
-
-    # TODO: add hookimpl methods for get_row_template, get_embed_template, get_fullpage_template, etc...
diff --git a/archivebox/abx/archivebox/hookspec.py b/archivebox/abx/archivebox/hookspec.py
deleted file mode 100644
index bfcb93b8..00000000
--- a/archivebox/abx/archivebox/hookspec.py
+++ /dev/null
@@ -1,52 +0,0 @@
-__package__ = 'abx.archivebox'
-
-from typing import Dict, Any
-
-from .. import hookspec
-
-from .base_binary import BaseBinary, BaseBinProvider
-from .base_configset import BaseConfigSet
-from .base_extractor import BaseExtractor
-from .base_searchbackend import BaseSearchBackend
-
-
-@hookspec
-def get_PLUGIN() -> Dict[str, Dict[str, Any]]:
-    return {}
-
-@hookspec
-def get_CONFIG() -> Dict[str, BaseConfigSet]:
-    return {}
-
-
-
-@hookspec
-def get_EXTRACTORS() -> Dict[str, BaseExtractor]:
-    return {}
-
-@hookspec
-def get_SEARCHBACKENDS() -> Dict[str, BaseSearchBackend]:
-    return {}
-
-# @hookspec
-# def get_REPLAYERS() -> Dict[str, BaseReplayer]:
-#     return {}
-
-# @hookspec
-# def get_ADMINDATAVIEWS():
-#     return {}
-
-# @hookspec
-# def get_QUEUES():
-#     return {}
-
-
-##############################################################
-# provided by abx.pydantic_pkgr.hookspec:
-# @hookspec
-# def get_BINARIES() -> Dict[str, BaseBinary]:
-#     return {}
-
-# @hookspec
-# def get_BINPROVIDERS() -> Dict[str, BaseBinProvider]:
-#     return {}
diff --git a/archivebox/abx/archivebox/reads.py b/archivebox/abx/archivebox/reads.py
deleted file mode 100644
index 10ad6ecd..00000000
--- a/archivebox/abx/archivebox/reads.py
+++ /dev/null
@@ -1,160 +0,0 @@
-__package__ = 'abx.archivebox'
-
-import importlib
-from typing import Dict, Set, Any, TYPE_CHECKING
-
-from benedict import benedict
-
-import abx
-from .. import pm
-
-if TYPE_CHECKING:
-    from .base_configset import BaseConfigSet
-    from .base_binary import BaseBinary, BaseBinProvider
-    from .base_extractor import BaseExtractor
-    from .base_searchbackend import BaseSearchBackend
-    # from .base_replayer import BaseReplayer
-    # from .base_queue import BaseQueue
-    # from .base_admindataview import BaseAdminDataView
-
-# API exposed to ArchiveBox code
-
-def get_PLUGINS() -> Dict[str, Dict[str, Any]]:
-    return benedict({
-        plugin_id: plugin
-        for plugin_dict in pm.hook.get_PLUGIN()
-            for plugin_id, plugin in plugin_dict.items()
-    })
-
-def get_PLUGIN(plugin_id: str) -> Dict[str, Any]:
-    plugin_info = get_PLUGINS().get(plugin_id, {})
-    package = plugin_info.get('package', plugin_info.get('PACKAGE', None))
-    if not package:
-        return {'id': plugin_id, 'hooks': {}}
-    module = importlib.import_module(package)
-    hooks = abx.get_plugin_hooks(module.__package__)
-    assert plugin_info and (plugin_info.get('id') or plugin_info.get('ID') or hooks)
-    
-    return benedict({
-        'id': plugin_id,
-        'label': getattr(module, '__label__', plugin_id),
-        'module': module,
-        'package': module.__package__,
-        'hooks': hooks,
-        'version': getattr(module, '__version__', '999.999.999'),
-        'author': getattr(module, '__author__', 'Unknown'),
-        'homepage': getattr(module, '__homepage__', 'https://github.com/ArchiveBox/ArchiveBox'),
-        'dependencies': getattr(module, '__dependencies__', []),
-        'source_code': module.__file__,
-        **plugin_info,
-    })
-    
-
-def get_HOOKS() -> Set[str]:
-    return {
-        hook_name
-        for plugin_id in get_PLUGINS().keys()
-            for hook_name in get_PLUGIN(plugin_id).hooks
-    }
-
-def get_CONFIGS() -> benedict:   # Dict[str, 'BaseConfigSet']
-    return benedict({
-        config_id: configset
-        for plugin_configs in pm.hook.get_CONFIG()
-            for config_id, configset in plugin_configs.items()
-    })
-
-
-def get_FLAT_CONFIG() -> Dict[str, Any]:
-    return benedict({
-        key: value
-        for configset in get_CONFIGS().values()
-            for key, value in configset.model_dump().items()
-    })
-
-def get_BINPROVIDERS() -> Dict[str, 'BaseBinProvider']:
-    # TODO: move these to plugins
-    from abx.archivebox.base_binary import apt, brew, env
-    builtin_binproviders = {
-        'env': env,
-        'apt': apt,
-        'brew': brew,
-    }
-    
-    return benedict({
-        binprovider_id: binprovider
-        for plugin_binproviders in [builtin_binproviders, *pm.hook.get_BINPROVIDERS()]
-            for binprovider_id, binprovider in plugin_binproviders.items()
-    })
-
-def get_BINARIES() -> Dict[str, 'BaseBinary']:
-    return benedict({
-        binary_id: binary
-        for plugin_binaries in pm.hook.get_BINARIES()
-            for binary_id, binary in plugin_binaries.items()
-    })
-
-def get_EXTRACTORS() -> Dict[str, 'BaseExtractor']:
-    return benedict({
-        extractor_id: extractor
-        for plugin_extractors in pm.hook.get_EXTRACTORS()
-            for extractor_id, extractor in plugin_extractors.items()
-    })
-
-# def get_REPLAYERS() -> Dict[str, 'BaseReplayer']:
-#     return benedict({
-#         replayer.id: replayer
-#         for plugin_replayers in pm.hook.get_REPLAYERS()
-#             for replayer in plugin_replayers
-#     })
-
-# def get_ADMINDATAVIEWS() -> Dict[str, 'BaseAdminDataView']:
-#     return benedict({
-#         admin_dataview.id: admin_dataview
-#         for plugin_admin_dataviews in pm.hook.get_ADMINDATAVIEWS()
-#             for admin_dataview in plugin_admin_dataviews
-#     })
-
-# def get_QUEUES() -> Dict[str, 'BaseQueue']:
-#     return benedict({
-#         queue.id: queue
-#         for plugin_queues in pm.hook.get_QUEUES()
-#             for queue in plugin_queues
-#     })
-
-def get_SEARCHBACKENDS() -> Dict[str, 'BaseSearchBackend']:
-    return benedict({
-        searchbackend_id: searchbackend
-        for plugin_searchbackends in pm.hook.get_SEARCHBACKENDS()
-            for searchbackend_id,searchbackend in plugin_searchbackends.items()
-    })
-
-
-
-def get_scope_config(defaults: benedict | None = None, persona=None, seed=None, crawl=None, snapshot=None, archiveresult=None, extra_config=None):
-    """Get all the relevant config for the given scope, in correct precedence order"""
-    
-    from django.conf import settings
-    default_config: benedict = defaults or settings.CONFIG
-    
-    snapshot = snapshot or (archiveresult and archiveresult.snapshot)
-    crawl = crawl or (snapshot and snapshot.crawl)
-    seed = seed or (crawl and crawl.seed)
-    persona = persona or (crawl and crawl.persona)
-    
-    persona_config = persona.config if persona else {}
-    seed_config = seed.config if seed else {}
-    crawl_config = crawl.config if crawl else {}
-    snapshot_config = snapshot.config if snapshot else {}
-    archiveresult_config = archiveresult.config if archiveresult else {}
-    extra_config = extra_config or {}
-    
-    return {
-        **default_config,               # defaults / config file / environment variables
-        **persona_config,               # lowest precedence
-        **seed_config,
-        **crawl_config,
-        **snapshot_config,
-        **archiveresult_config,
-        **extra_config,                 # highest precedence
-    }
diff --git a/archivebox/abx/django/__init__.py b/archivebox/abx/django/__init__.py
deleted file mode 100644
index 56fe8ddd..00000000
--- a/archivebox/abx/django/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__package__ = 'abx.django'
diff --git a/archivebox/abx/django/use.py b/archivebox/abx/django/use.py
deleted file mode 100644
index a52ada3b..00000000
--- a/archivebox/abx/django/use.py
+++ /dev/null
@@ -1,101 +0,0 @@
-__package__ = 'abx.django'
-
-import itertools
-# from benedict import benedict
-
-from .. import pm
-
-
-def get_INSTALLED_APPS():
-    return itertools.chain(*reversed(pm.hook.get_INSTALLED_APPS()))
-
-# def register_INSTALLLED_APPS(INSTALLED_APPS):
-#     pm.hook.register_INSTALLED_APPS(INSTALLED_APPS=INSTALLED_APPS)
-
-
-def get_MIDDLEWARES():
-    return itertools.chain(*reversed(pm.hook.get_MIDDLEWARE()))
-
-# def register_MIDDLEWARES(MIDDLEWARE):
-#     pm.hook.register_MIDDLEWARE(MIDDLEWARE=MIDDLEWARE)
-
-
-def get_AUTHENTICATION_BACKENDS():
-    return itertools.chain(*reversed(pm.hook.get_AUTHENTICATION_BACKENDS()))
-
-# def register_AUTHENTICATION_BACKENDS(AUTHENTICATION_BACKENDS):
-#     pm.hook.register_AUTHENTICATION_BACKENDS(AUTHENTICATION_BACKENDS=AUTHENTICATION_BACKENDS)
-
-
-def get_STATICFILES_DIRS():
-    return itertools.chain(*reversed(pm.hook.get_STATICFILES_DIRS()))
-
-# def register_STATICFILES_DIRS(STATICFILES_DIRS):
-#     pm.hook.register_STATICFILES_DIRS(STATICFILES_DIRS=STATICFILES_DIRS)
-
-
-def get_TEMPLATE_DIRS():
-    return itertools.chain(*reversed(pm.hook.get_TEMPLATE_DIRS()))
-
-# def register_TEMPLATE_DIRS(TEMPLATE_DIRS):
-#     pm.hook.register_TEMPLATE_DIRS(TEMPLATE_DIRS=TEMPLATE_DIRS)
-
-def get_DJANGO_HUEY_QUEUES(QUEUE_DATABASE_NAME='queue.sqlite3'):
-    HUEY_QUEUES = {}
-    for plugin_result in pm.hook.get_DJANGO_HUEY_QUEUES(QUEUE_DATABASE_NAME=QUEUE_DATABASE_NAME):
-        HUEY_QUEUES.update(plugin_result)
-    return HUEY_QUEUES
-
-# def register_DJANGO_HUEY(DJANGO_HUEY):
-#     pm.hook.register_DJANGO_HUEY(DJANGO_HUEY=DJANGO_HUEY)
-
-def get_ADMIN_DATA_VIEWS_URLS():
-    return itertools.chain(*reversed(pm.hook.get_ADMIN_DATA_VIEWS_URLS()))
-
-# def register_ADMIN_DATA_VIEWS(ADMIN_DATA_VIEWS):
-#     pm.hook.register_ADMIN_DATA_VIEWS(ADMIN_DATA_VIEWS=ADMIN_DATA_VIEWS)
-
-
-# def register_settings(settings):
-#     # convert settings dict to an benedict so we can set values using settings.attr = xyz notation
-#     settings_as_obj = benedict(settings, keypath_separator=None)
-    
-#     # set default values for settings that are used by plugins
-#     # settings_as_obj.INSTALLED_APPS = settings_as_obj.get('INSTALLED_APPS', [])
-#     # settings_as_obj.MIDDLEWARE = settings_as_obj.get('MIDDLEWARE', [])
-#     # settings_as_obj.AUTHENTICATION_BACKENDS = settings_as_obj.get('AUTHENTICATION_BACKENDS', [])
-#     # settings_as_obj.STATICFILES_DIRS = settings_as_obj.get('STATICFILES_DIRS', [])
-#     # settings_as_obj.TEMPLATE_DIRS = settings_as_obj.get('TEMPLATE_DIRS', [])
-#     # settings_as_obj.DJANGO_HUEY = settings_as_obj.get('DJANGO_HUEY', {'queues': {}})
-#     # settings_as_obj.ADMIN_DATA_VIEWS = settings_as_obj.get('ADMIN_DATA_VIEWS', {'URLS': []})
-    
-#     # # call all the hook functions to mutate the settings values in-place
-#     # register_INSTALLLED_APPS(settings_as_obj.INSTALLED_APPS)
-#     # register_MIDDLEWARES(settings_as_obj.MIDDLEWARE)
-#     # register_AUTHENTICATION_BACKENDS(settings_as_obj.AUTHENTICATION_BACKENDS)
-#     # register_STATICFILES_DIRS(settings_as_obj.STATICFILES_DIRS)
-#     # register_TEMPLATE_DIRS(settings_as_obj.TEMPLATE_DIRS)
-#     # register_DJANGO_HUEY(settings_as_obj.DJANGO_HUEY)
-#     # register_ADMIN_DATA_VIEWS(settings_as_obj.ADMIN_DATA_VIEWS)
-    
-#     # calls Plugin.settings(settings) on each registered plugin
-#     pm.hook.register_settings(settings=settings_as_obj)
-    
-#     # then finally update the settings globals() object will all the new settings
-#     # settings.update(settings_as_obj)
-
-
-def get_urlpatterns():
-    return list(itertools.chain(*pm.hook.urlpatterns()))
-
-def register_urlpatterns(urlpatterns):
-    pm.hook.register_urlpatterns(urlpatterns=urlpatterns)
-
-
-def register_checks():
-    """register any django system checks"""
-    pm.hook.register_checks()
-
-def register_admin(admin_site):
-    """register any django admin models/views with the main django admin site instance"""
-    pm.hook.register_admin(admin_site=admin_site)
diff --git a/archivebox/abx/hookspec.py b/archivebox/abx/hookspec.py
deleted file mode 100644
index a25f7673..00000000
--- a/archivebox/abx/hookspec.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from pathlib import Path
-
-from pluggy import HookimplMarker
-from pluggy import HookspecMarker
-
-spec = hookspec = HookspecMarker("abx")
-impl = hookimpl = HookimplMarker("abx")
-
-
-@hookspec
-@hookimpl
-def get_system_user() -> str:
-    # Beware $HOME may not match current EUID, UID, PUID, SUID, there are edge cases
-    # - sudo (EUD != UID != SUID)
-    # - running with an autodetected UID based on data dir ownership
-    #   but mapping of UID:username is broken because it was created
-    #   by a different host system, e.g. 911's $HOME outside of docker
-    #   might be /usr/lib/lxd instead of /home/archivebox
-    # - running as a user that doens't have a home directory
-    # - home directory is set to a path that doesn't exist, or is inside a dir we cant read
-    return Path('~').expanduser().name
-
diff --git a/archivebox/abx/manager.py b/archivebox/abx/manager.py
deleted file mode 100644
index 8d44a087..00000000
--- a/archivebox/abx/manager.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import inspect
-
-import pluggy
-
-
-class PluginManager(pluggy.PluginManager):
-    """
-    Patch to fix pluggy's PluginManager to work with pydantic models.
-    See: https://github.com/pytest-dev/pluggy/pull/536
-    """
-    def parse_hookimpl_opts(self, plugin, name: str) -> pluggy.HookimplOpts | None:
-        # IMPORTANT: @property methods can have side effects, and are never hookimpl
-        # if attr is a property, skip it in advance
-        plugin_class = plugin if inspect.isclass(plugin) else type(plugin)
-        if isinstance(getattr(plugin_class, name, None), property):
-            return None
-
-        # pydantic model fields are like attrs and also can never be hookimpls
-        plugin_is_pydantic_obj = hasattr(plugin, "__pydantic_core_schema__")
-        if plugin_is_pydantic_obj and name in getattr(plugin, "model_fields", {}):
-            # pydantic models mess with the class and attr __signature__
-            # so inspect.isroutine(...) throws exceptions and cant be used
-            return None
-        
-        try:
-            return super().parse_hookimpl_opts(plugin, name)
-        except AttributeError:
-            return super().parse_hookimpl_opts(type(plugin), name)
-
-pm = PluginManager("abx")
diff --git a/archivebox/abx/pydantic_pkgr/__init__.py b/archivebox/abx/pydantic_pkgr/__init__.py
deleted file mode 100644
index 28cd0f81..00000000
--- a/archivebox/abx/pydantic_pkgr/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__package__ = 'abx.pydantic_pkgr'
diff --git a/archivebox/abx/pydantic_pkgr/hookspec.py b/archivebox/abx/pydantic_pkgr/hookspec.py
deleted file mode 100644
index 6b293abb..00000000
--- a/archivebox/abx/pydantic_pkgr/hookspec.py
+++ /dev/null
@@ -1,13 +0,0 @@
-
-from ..hookspec import hookspec
-
-###########################################################################################
-
-@hookspec
-def get_BINPROVIDERS():
-    return {}
-
-@hookspec
-def get_BINARIES():
-    return {}
-
diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py
index 2b9e7edb..88858156 100644
--- a/archivebox/core/settings.py
+++ b/archivebox/core/settings.py
@@ -9,9 +9,6 @@ from pathlib import Path
 from django.utils.crypto import get_random_string
 
 import abx
-import abx.archivebox
-import abx.archivebox.reads
-import abx.django.use
 
 from archivebox.config import DATA_DIR, PACKAGE_DIR, ARCHIVE_DIR, CONSTANTS
 from archivebox.config.common import SHELL_CONFIG, SERVER_CONFIG      # noqa
@@ -26,43 +23,22 @@ IS_GETTING_VERSION_OR_HELP = 'version' in sys.argv or 'help' in sys.argv or '--v
 ################################################################################
 
 PLUGIN_HOOKSPECS = [
-    'abx.django.hookspec',
-    'abx.pydantic_pkgr.hookspec',
-    'abx.archivebox.hookspec',
+    'abx_spec_django',
+    'abx_spec_pydantic_pkgr',
+    'abx_spec_config',
+    'abx_spec_archivebox',
 ]
 abx.register_hookspecs(PLUGIN_HOOKSPECS)
 
-BUILTIN_PLUGIN_DIRS = {
-    'archivebox':              PACKAGE_DIR,
-    'plugins_pkg':             PACKAGE_DIR / 'plugins_pkg',
-    'plugins_auth':            PACKAGE_DIR / 'plugins_auth',
-    'plugins_search':          PACKAGE_DIR / 'plugins_search',
-    'plugins_extractor':       PACKAGE_DIR / 'plugins_extractor',
-}
-USER_PLUGIN_DIRS = {
-    # 'user_plugins':            DATA_DIR / 'user_plugins',
-}
+SYSTEM_PLUGINS = abx.get_pip_installed_plugins(group='abx')
+USER_PLUGINS = abx.find_plugins_in_dir(DATA_DIR / 'user_plugins')
 
-# Discover ArchiveBox plugins
-BUILTIN_PLUGINS = abx.get_plugins_in_dirs(BUILTIN_PLUGIN_DIRS)
-PIP_PLUGINS = abx.get_pip_installed_plugins(group='archivebox')
-USER_PLUGINS = abx.get_plugins_in_dirs(USER_PLUGIN_DIRS)
-ALL_PLUGINS = {**BUILTIN_PLUGINS, **PIP_PLUGINS, **USER_PLUGINS}
+ALL_PLUGINS = {**SYSTEM_PLUGINS, **USER_PLUGINS}
 
 # Load ArchiveBox plugins
-PLUGIN_MANAGER = abx.pm
-abx.archivebox.load_archivebox_plugins(PLUGIN_MANAGER, ALL_PLUGINS)
-PLUGINS = abx.archivebox.reads.get_PLUGINS()
+abx.load_plugins(ALL_PLUGINS)
 
-# Load ArchiveBox config from plugins
-CONFIGS = abx.archivebox.reads.get_CONFIGS()
-CONFIG = FLAT_CONFIG = abx.archivebox.reads.get_FLAT_CONFIG()
-BINPROVIDERS = abx.archivebox.reads.get_BINPROVIDERS()
-BINARIES = abx.archivebox.reads.get_BINARIES()
-EXTRACTORS = abx.archivebox.reads.get_EXTRACTORS()
-SEARCHBACKENDS = abx.archivebox.reads.get_SEARCHBACKENDS()
-# REPLAYERS = abx.archivebox.reads.get_REPLAYERS()
-# ADMINDATAVIEWS = abx.archivebox.reads.get_ADMINDATAVIEWS()
+# # Load ArchiveBox config from plugins
 
 
 ################################################################################
@@ -110,7 +86,7 @@ INSTALLED_APPS = [
     'api',                       # Django-Ninja-based Rest API interfaces, config, APIToken model, etc.
 
     # ArchiveBox plugins
-    *abx.django.use.get_INSTALLED_APPS(),  # all plugin django-apps found in archivebox/plugins_* and data/user_plugins,
+    *abx.as_list(abx.pm.hook.get_INSTALLED_APPS()),  # all plugin django-apps found in archivebox/plugins_* and data/user_plugins,
 
     # 3rd-party apps from PyPI that need to be loaded last
     'admin_data_views',          # handles rendering some convenient automatic read-only views of data in Django admin
@@ -135,7 +111,7 @@ MIDDLEWARE = [
     'core.middleware.ReverseProxyAuthMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'core.middleware.CacheControlMiddleware',
-    *abx.django.use.get_MIDDLEWARES(),
+    *abx.as_list(abx.pm.hook.get_MIDDLEWARES()),
 ]
 
 
@@ -148,7 +124,7 @@ MIDDLEWARE = [
 AUTHENTICATION_BACKENDS = [
     'django.contrib.auth.backends.RemoteUserBackend',
     'django.contrib.auth.backends.ModelBackend',
-    *abx.django.use.get_AUTHENTICATION_BACKENDS(),
+    *abx.as_list(abx.pm.hook.get_AUTHENTICATION_BACKENDS()),
 ]
 
 
@@ -169,7 +145,7 @@ AUTHENTICATION_BACKENDS = [
 
 STATIC_URL = '/static/'
 TEMPLATES_DIR_NAME = 'templates'
-CUSTOM_TEMPLATES_ENABLED = os.access(CONSTANTS.CUSTOM_TEMPLATES_DIR, os.R_OK) and CONSTANTS.CUSTOM_TEMPLATES_DIR.is_dir()
+CUSTOM_TEMPLATES_ENABLED = os.path.isdir(CONSTANTS.CUSTOM_TEMPLATES_DIR) and os.access(CONSTANTS.CUSTOM_TEMPLATES_DIR, os.R_OK)
 STATICFILES_DIRS = [
     *([str(CONSTANTS.CUSTOM_TEMPLATES_DIR / 'static')] if CUSTOM_TEMPLATES_ENABLED else []),
     # *[
@@ -177,7 +153,7 @@ STATICFILES_DIRS = [
     #     for plugin_dir in PLUGIN_DIRS.values()
     #     if (plugin_dir / 'static').is_dir()
     # ],
-    *abx.django.use.get_STATICFILES_DIRS(),
+    *abx.as_list(abx.pm.hook.get_STATICFILES_DIRS()),
     str(PACKAGE_DIR / TEMPLATES_DIR_NAME / 'static'),
 ]
 
@@ -188,7 +164,7 @@ TEMPLATE_DIRS = [
     #     for plugin_dir in PLUGIN_DIRS.values()
     #     if (plugin_dir / 'templates').is_dir()
     # ],
-    *abx.django.use.get_TEMPLATE_DIRS(),
+    *abx.as_list(abx.pm.hook.get_TEMPLATE_DIRS()),
     str(PACKAGE_DIR / TEMPLATES_DIR_NAME / 'core'),
     str(PACKAGE_DIR / TEMPLATES_DIR_NAME / 'admin'),
     str(PACKAGE_DIR / TEMPLATES_DIR_NAME),
@@ -292,7 +268,7 @@ if not IS_GETTING_VERSION_OR_HELP:             # dont create queue.sqlite3 file
         "queues": {
             HUEY["name"]: HUEY.copy(),
             # more registered here at plugin import-time by BaseQueue.register()
-            **abx.django.use.get_DJANGO_HUEY_QUEUES(QUEUE_DATABASE_NAME=CONSTANTS.QUEUE_DATABASE_FILENAME),
+            **abx.as_dict(abx.pm.hook.get_DJANGO_HUEY_QUEUES(QUEUE_DATABASE_NAME=CONSTANTS.QUEUE_DATABASE_FILENAME)),
         },
     }
 
@@ -517,7 +493,7 @@ ADMIN_DATA_VIEWS = {
                 "name": "log",
             },
         },
-        *abx.django.use.get_ADMIN_DATA_VIEWS_URLS(),
+        *abx.as_list(abx.pm.hook.get_ADMIN_DATA_VIEWS_URLS()),
     ],
 }
 
@@ -611,7 +587,4 @@ if DEBUG_REQUESTS_TRACKER:
 # JET_TOKEN = 'some-api-token-here'
 
 
-abx.django.use.register_checks()
-# abx.archivebox.reads.register_all_hooks(globals())
-
 # import ipdb; ipdb.set_trace()
diff --git a/archivebox/plugins_pkg/npm/binproviders.py b/archivebox/plugins_pkg/npm/binproviders.py
deleted file mode 100644
index b1b83168..00000000
--- a/archivebox/plugins_pkg/npm/binproviders.py
+++ /dev/null
@@ -1,42 +0,0 @@
-__package__ = 'plugins_pkg.npm'
-
-from pathlib import Path
-from typing import Optional
-
-from pydantic_pkgr import NpmProvider, PATHStr, BinProviderName
-
-from archivebox.config import DATA_DIR, CONSTANTS
-
-from abx.archivebox.base_binary import BaseBinProvider
-
-
-
-OLD_NODE_BIN_PATH = DATA_DIR / 'node_modules' / '.bin'
-NEW_NODE_BIN_PATH = CONSTANTS.DEFAULT_LIB_DIR / 'npm' / 'node_modules' / '.bin'
-
-
-class SystemNpmBinProvider(NpmProvider, BaseBinProvider):
-    name: BinProviderName = "sys_npm"
-    
-    npm_prefix: Optional[Path] = None
-
-
-class LibNpmBinProvider(NpmProvider, BaseBinProvider):
-    name: BinProviderName = "lib_npm"
-    PATH: PATHStr = f'{NEW_NODE_BIN_PATH}:{OLD_NODE_BIN_PATH}'
-    
-    npm_prefix: Optional[Path] = CONSTANTS.DEFAULT_LIB_DIR / 'npm'
-    
-    def setup(self) -> None:
-        # update paths from config if they arent the default
-        from archivebox.config.common import STORAGE_CONFIG
-        if STORAGE_CONFIG.LIB_DIR != CONSTANTS.DEFAULT_LIB_DIR:
-            self.npm_prefix = STORAGE_CONFIG.LIB_DIR / 'npm'
-            self.PATH = f'{STORAGE_CONFIG.LIB_DIR / "npm" / "node_modules" / ".bin"}:{NEW_NODE_BIN_PATH}:{OLD_NODE_BIN_PATH}'
-
-        super().setup()
-
-
-SYS_NPM_BINPROVIDER = SystemNpmBinProvider()
-LIB_NPM_BINPROVIDER = LibNpmBinProvider()
-npm = LIB_NPM_BINPROVIDER
diff --git a/archivebox/vendor/__init__.py b/archivebox/vendor/__init__.py
index a997acbb..fcd93405 100644
--- a/archivebox/vendor/__init__.py
+++ b/archivebox/vendor/__init__.py
@@ -8,8 +8,8 @@ VENDORED_LIBS = {
     # sys.path dir:         library name
     #'python-atomicwrites':  'atomicwrites',
     #'django-taggit':        'taggit',
-    'pydantic-pkgr':        'pydantic_pkgr',
-    'pocket':               'pocket',
+    # 'pydantic-pkgr':        'pydantic_pkgr',
+    # 'pocket':               'pocket',
     #'base32-crockford':     'base32_crockford',
 }
 
diff --git a/archivebox/vendor/pocket b/archivebox/vendor/pocket
deleted file mode 160000
index e7970b63..00000000
--- a/archivebox/vendor/pocket
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit e7970b63feafc8941c325111c5ce3706698a18b5
diff --git a/archivebox/vendor/pydantic-pkgr b/archivebox/vendor/pydantic-pkgr
deleted file mode 160000
index a774f246..00000000
--- a/archivebox/vendor/pydantic-pkgr
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit a774f24644ee14f14fa2cc3d8e6e0a585ae00fdd
diff --git a/click_test.py b/click_test.py
new file mode 100644
index 00000000..52d1d6e1
--- /dev/null
+++ b/click_test.py
@@ -0,0 +1,32 @@
+import sys
+import click
+from rich import print
+from archivebox.config.django import setup_django
+
+setup_django()
+
+import abx.archivebox.writes
+
+
+def parse_stdin_to_args(io=sys.stdin):
+    for line in io.read().split('\n'):
+        for url_or_id in line.split(' '):
+            if url_or_id.strip():
+                yield url_or_id.strip()
+
+
+# Gather data from stdin in case using a pipe
+if not sys.stdin.isatty():
+    sys.argv += parse_stdin_to_args(sys.stdin)
+
+
+@click.command()
+@click.argument("snapshot_ids_or_urls", type=str, nargs=-1)
+def extract(snapshot_ids_or_urls):
+    for url_or_snapshot_id in snapshot_ids_or_urls:
+        print('- EXTRACTING', url_or_snapshot_id, file=sys.stderr)
+        for result in abx.archivebox.writes.extract(url_or_snapshot_id):
+            print(result)
+
+if __name__ == "__main__":
+    extract()
diff --git a/archivebox/plugins_auth/__init__.py b/packages/abx-plugin-archivedotorg-extractor/README.md
similarity index 100%
rename from archivebox/plugins_auth/__init__.py
rename to packages/abx-plugin-archivedotorg-extractor/README.md
diff --git a/archivebox/plugins_extractor/archivedotorg/__init__.py b/packages/abx-plugin-archivedotorg-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/archivedotorg/__init__.py
rename to packages/abx-plugin-archivedotorg-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/archivedotorg/config.py b/packages/abx-plugin-archivedotorg-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/archivedotorg/config.py
rename to packages/abx-plugin-archivedotorg-extractor/config.py
diff --git a/packages/abx-plugin-archivedotorg-extractor/pyproject.toml b/packages/abx-plugin-archivedotorg-extractor/pyproject.toml
new file mode 100644
index 00000000..8754b4bd
--- /dev/null
+++ b/packages/abx-plugin-archivedotorg-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-archivedotorg-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_extractor/__init__.py b/packages/abx-plugin-chrome-extractor/README.md
similarity index 100%
rename from archivebox/plugins_extractor/__init__.py
rename to packages/abx-plugin-chrome-extractor/README.md
diff --git a/archivebox/plugins_extractor/chrome/__init__.py b/packages/abx-plugin-chrome-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/chrome/__init__.py
rename to packages/abx-plugin-chrome-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/chrome/binaries.py b/packages/abx-plugin-chrome-extractor/binaries.py
similarity index 84%
rename from archivebox/plugins_extractor/chrome/binaries.py
rename to packages/abx-plugin-chrome-extractor/binaries.py
index 59573d93..a79b66a2 100644
--- a/archivebox/plugins_extractor/chrome/binaries.py
+++ b/packages/abx-plugin-chrome-extractor/binaries.py
@@ -13,15 +13,15 @@ from pydantic_pkgr import (
     bin_abspath,
 )
 
+import abx.archivebox.reads
 from abx.archivebox.base_binary import BaseBinary, env, apt, brew
 
-# Depends on Other Plugins:
-from archivebox.config.common import SHELL_CONFIG
-from plugins_pkg.puppeteer.binproviders import PUPPETEER_BINPROVIDER
-from plugins_pkg.playwright.binproviders import PLAYWRIGHT_BINPROVIDER
+from abx_puppeteer_binprovider.binproviders import PUPPETEER_BINPROVIDER
+from abx_playwright_binprovider.binproviders import PLAYWRIGHT_BINPROVIDER
 
 
 from .config import CHROME_CONFIG
+
 CHROMIUM_BINARY_NAMES_LINUX = [
     "chromium",
     "chromium-browser",
@@ -48,12 +48,13 @@ CHROME_BINARY_NAMES_MACOS = [
 ]
 CHROME_BINARY_NAMES = CHROME_BINARY_NAMES_LINUX + CHROME_BINARY_NAMES_MACOS
 
-APT_DEPENDENCIES = [
-    'apt-transport-https', 'at-spi2-common', 'chromium-browser',
+CHROME_APT_DEPENDENCIES = [
+    'apt-transport-https', 'at-spi2-common',
     'fontconfig', 'fonts-freefont-ttf', 'fonts-ipafont-gothic', 'fonts-kacst', 'fonts-khmeros', 'fonts-liberation', 'fonts-noto', 'fonts-noto-color-emoji', 'fonts-symbola', 'fonts-thai-tlwg', 'fonts-tlwg-loma-otf', 'fonts-unifont', 'fonts-wqy-zenhei',
     'libasound2', 'libatk-bridge2.0-0', 'libatk1.0-0', 'libatspi2.0-0', 'libavahi-client3', 'libavahi-common-data', 'libavahi-common3', 'libcairo2', 'libcups2',
     'libdbus-1-3', 'libdrm2', 'libfontenc1', 'libgbm1', 'libglib2.0-0', 'libice6', 'libnspr4', 'libnss3', 'libsm6', 'libunwind8', 'libx11-6', 'libxaw7', 'libxcb1',
     'libxcomposite1', 'libxdamage1', 'libxext6', 'libxfixes3', 'libxfont2', 'libxkbcommon0', 'libxkbfile1', 'libxmu6', 'libxpm4', 'libxrandr2', 'libxt6', 'x11-utils', 'x11-xkb-utils', 'xfonts-encodings',
+    'chromium-browser',
 ]
 
 
@@ -95,7 +96,7 @@ class ChromeBinary(BaseBinary):
             'packages': ['chromium'],                   # playwright install chromium
         },
         apt.name: {
-            'packages': APT_DEPENDENCIES,
+            'packages': CHROME_APT_DEPENDENCIES,
         },
         brew.name: {
             'packages': ['--cask', 'chromium'] if platform.system().lower() == 'darwin' else [],
@@ -104,10 +105,9 @@ class ChromeBinary(BaseBinary):
 
     @staticmethod
     def symlink_to_lib(binary, bin_dir=None) -> None:
-        from archivebox.config.common import STORAGE_CONFIG
-        bin_dir = bin_dir or STORAGE_CONFIG.LIB_DIR / 'bin'
+        bin_dir = bin_dir or abx.archivebox.reads.get_CONFIGS().STORAGE_CONFIG.LIB_DIR / 'bin'
         
-        if not (binary.abspath and os.access(binary.abspath, os.F_OK)):
+        if not (binary.abspath and os.path.isfile(binary.abspath)):
             return
         
         bin_dir.mkdir(parents=True, exist_ok=True)
@@ -121,7 +121,7 @@ class ChromeBinary(BaseBinary):
                 # otherwise on linux we can symlink directly to binary executable
                 symlink.unlink(missing_ok=True)
                 symlink.symlink_to(binary.abspath)
-        except Exception as err:
+        except Exception:
             # print(f'[red]:warning: Failed to symlink {symlink} -> {binary.abspath}[/red] {err}')
             # not actually needed, we can just run without it
             pass
@@ -132,14 +132,17 @@ class ChromeBinary(BaseBinary):
         Cleans up any state or runtime files that chrome leaves behind when killed by
         a timeout or other error
         """
-        lock_file = Path("~/.config/chromium/SingletonLock").expanduser()
-
-        if SHELL_CONFIG.IN_DOCKER and os.access(lock_file, os.F_OK):
-            lock_file.unlink()
+        try:
+            linux_lock_file = Path("~/.config/chromium/SingletonLock").expanduser()
+            linux_lock_file.unlink(missing_ok=True)
+        except Exception:
+            pass
         
         if CHROME_CONFIG.CHROME_USER_DATA_DIR:
-            if os.access(CHROME_CONFIG.CHROME_USER_DATA_DIR / 'SingletonLock', os.F_OK):
-                lock_file.unlink()
+            try:
+                (CHROME_CONFIG.CHROME_USER_DATA_DIR / 'SingletonLock').unlink(missing_ok=True)
+            except Exception:
+                pass
 
 
 
diff --git a/archivebox/plugins_extractor/chrome/config.py b/packages/abx-plugin-chrome-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/chrome/config.py
rename to packages/abx-plugin-chrome-extractor/config.py
diff --git a/packages/abx-plugin-chrome-extractor/pyproject.toml b/packages/abx-plugin-chrome-extractor/pyproject.toml
new file mode 100644
index 00000000..6676882c
--- /dev/null
+++ b/packages/abx-plugin-chrome-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-chrome-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_pkg/__init__.py b/packages/abx-plugin-curl-extractor/README.md
similarity index 100%
rename from archivebox/plugins_pkg/__init__.py
rename to packages/abx-plugin-curl-extractor/README.md
diff --git a/archivebox/plugins_extractor/curl/__init__.py b/packages/abx-plugin-curl-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/curl/__init__.py
rename to packages/abx-plugin-curl-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/curl/binaries.py b/packages/abx-plugin-curl-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/curl/binaries.py
rename to packages/abx-plugin-curl-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/curl/config.py b/packages/abx-plugin-curl-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/curl/config.py
rename to packages/abx-plugin-curl-extractor/config.py
diff --git a/packages/abx-plugin-curl-extractor/pyproject.toml b/packages/abx-plugin-curl-extractor/pyproject.toml
new file mode 100644
index 00000000..9bd6f396
--- /dev/null
+++ b/packages/abx-plugin-curl-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-curl-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_search/__init__.py b/packages/abx-plugin-default-binproviders/README.md
similarity index 100%
rename from archivebox/plugins_search/__init__.py
rename to packages/abx-plugin-default-binproviders/README.md
diff --git a/packages/abx-plugin-default-binproviders/abx_plugin_default_binproviders.py b/packages/abx-plugin-default-binproviders/abx_plugin_default_binproviders.py
new file mode 100644
index 00000000..2a628a4e
--- /dev/null
+++ b/packages/abx-plugin-default-binproviders/abx_plugin_default_binproviders.py
@@ -0,0 +1,24 @@
+
+import abx
+
+from typing import Dict
+
+from pydantic_pkgr import (
+    AptProvider,
+    BrewProvider,
+    EnvProvider,
+    BinProvider,
+)
+apt = APT_BINPROVIDER = AptProvider()
+brew = BREW_BINPROVIDER = BrewProvider()
+env = ENV_BINPROVIDER = EnvProvider()
+
+
+@abx.hookimpl(tryfirst=True)
+def get_BINPROVIDERS() -> Dict[str, BinProvider]:
+
+    return {
+        'apt': APT_BINPROVIDER,
+        'brew': BREW_BINPROVIDER,
+        'env': ENV_BINPROVIDER,
+    }
diff --git a/packages/abx-plugin-default-binproviders/pyproject.toml b/packages/abx-plugin-default-binproviders/pyproject.toml
new file mode 100644
index 00000000..3f8fec96
--- /dev/null
+++ b/packages/abx-plugin-default-binproviders/pyproject.toml
@@ -0,0 +1,18 @@
+[project]
+name = "abx-plugin-default-binproviders"
+version = "2024.10.24"
+description = "Default BinProviders for ABX (apt, brew, env)"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "pydantic-pkgr>=0.5.4",
+    "abx-spec-pydantic-pkgr>=0.1.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_plugin_default_binproviders = "abx_plugin_default_binproviders"
diff --git a/packages/abx-plugin-favicon-extractor/README.md b/packages/abx-plugin-favicon-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/favicon/__init__.py b/packages/abx-plugin-favicon-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/favicon/__init__.py
rename to packages/abx-plugin-favicon-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/favicon/config.py b/packages/abx-plugin-favicon-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/favicon/config.py
rename to packages/abx-plugin-favicon-extractor/config.py
diff --git a/packages/abx-plugin-favicon-extractor/pyproject.toml b/packages/abx-plugin-favicon-extractor/pyproject.toml
new file mode 100644
index 00000000..96e62f6d
--- /dev/null
+++ b/packages/abx-plugin-favicon-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-favicon-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-git-extractor/README.md b/packages/abx-plugin-git-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/git/__init__.py b/packages/abx-plugin-git-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/git/__init__.py
rename to packages/abx-plugin-git-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/git/binaries.py b/packages/abx-plugin-git-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/git/binaries.py
rename to packages/abx-plugin-git-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/git/config.py b/packages/abx-plugin-git-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/git/config.py
rename to packages/abx-plugin-git-extractor/config.py
diff --git a/archivebox/plugins_extractor/git/extractors.py b/packages/abx-plugin-git-extractor/extractors.py
similarity index 100%
rename from archivebox/plugins_extractor/git/extractors.py
rename to packages/abx-plugin-git-extractor/extractors.py
diff --git a/packages/abx-plugin-git-extractor/pyproject.toml b/packages/abx-plugin-git-extractor/pyproject.toml
new file mode 100644
index 00000000..4a7b375e
--- /dev/null
+++ b/packages/abx-plugin-git-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-git-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-htmltotext-extractor/README.md b/packages/abx-plugin-htmltotext-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/htmltotext/__init__.py b/packages/abx-plugin-htmltotext-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/htmltotext/__init__.py
rename to packages/abx-plugin-htmltotext-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/htmltotext/config.py b/packages/abx-plugin-htmltotext-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/htmltotext/config.py
rename to packages/abx-plugin-htmltotext-extractor/config.py
diff --git a/packages/abx-plugin-htmltotext-extractor/pyproject.toml b/packages/abx-plugin-htmltotext-extractor/pyproject.toml
new file mode 100644
index 00000000..2e26cb25
--- /dev/null
+++ b/packages/abx-plugin-htmltotext-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-htmltotext-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-ldap-auth/README.md b/packages/abx-plugin-ldap-auth/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_auth/ldap/__init__.py b/packages/abx-plugin-ldap-auth/__init__.py
similarity index 100%
rename from archivebox/plugins_auth/ldap/__init__.py
rename to packages/abx-plugin-ldap-auth/__init__.py
diff --git a/archivebox/plugins_auth/ldap/binaries.py b/packages/abx-plugin-ldap-auth/binaries.py
similarity index 100%
rename from archivebox/plugins_auth/ldap/binaries.py
rename to packages/abx-plugin-ldap-auth/binaries.py
diff --git a/archivebox/plugins_auth/ldap/config.py b/packages/abx-plugin-ldap-auth/config.py
similarity index 100%
rename from archivebox/plugins_auth/ldap/config.py
rename to packages/abx-plugin-ldap-auth/config.py
diff --git a/packages/abx-plugin-ldap-auth/pyproject.toml b/packages/abx-plugin-ldap-auth/pyproject.toml
new file mode 100644
index 00000000..1db98ebd
--- /dev/null
+++ b/packages/abx-plugin-ldap-auth/pyproject.toml
@@ -0,0 +1,22 @@
+[project]
+name = "abx-ldap-auth"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
+
+
+[project.entry-points.abx]
+ldap = "abx_ldap_auth"
+
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.sdist]
+packages = ["."]
+
+[tool.hatch.build.targets.wheel]
+packages = ["."]
diff --git a/packages/abx-plugin-mercury-extractor/README.md b/packages/abx-plugin-mercury-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/mercury/__init__.py b/packages/abx-plugin-mercury-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/mercury/__init__.py
rename to packages/abx-plugin-mercury-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/mercury/binaries.py b/packages/abx-plugin-mercury-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/mercury/binaries.py
rename to packages/abx-plugin-mercury-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/mercury/config.py b/packages/abx-plugin-mercury-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/mercury/config.py
rename to packages/abx-plugin-mercury-extractor/config.py
diff --git a/archivebox/plugins_extractor/mercury/extractors.py b/packages/abx-plugin-mercury-extractor/extractors.py
similarity index 100%
rename from archivebox/plugins_extractor/mercury/extractors.py
rename to packages/abx-plugin-mercury-extractor/extractors.py
diff --git a/packages/abx-plugin-mercury-extractor/pyproject.toml b/packages/abx-plugin-mercury-extractor/pyproject.toml
new file mode 100644
index 00000000..35415a1d
--- /dev/null
+++ b/packages/abx-plugin-mercury-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-mercury-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-npm-binprovider/README.md b/packages/abx-plugin-npm-binprovider/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_pkg/npm/__init__.py b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/__init__.py
similarity index 63%
rename from archivebox/plugins_pkg/npm/__init__.py
rename to packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/__init__.py
index 921d42e4..3901516e 100644
--- a/archivebox/plugins_pkg/npm/__init__.py
+++ b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/__init__.py
@@ -1,26 +1,12 @@
-__package__ = 'plugins_pkg.npm'
-__version__ = '2024.10.14'
+__package__ = 'abx_plugin_npm_binprovider'
 __id__ = 'npm'
-__label__ = 'npm'
+__label__ = 'NPM'
 __author__ = 'ArchiveBox'
 __homepage__ = 'https://www.npmjs.com/'
 
 import abx
 
 
-@abx.hookimpl
-def get_PLUGIN():
-    return {
-        __id__: {
-            'id': __id__,
-            'package': __package__,
-            'label': __label__,
-            'version': __version__,
-            'author': __author__,
-            'homepage': __homepage__,
-        }
-    }
-
 @abx.hookimpl
 def get_CONFIG():
     from .config import NPM_CONFIG
diff --git a/archivebox/plugins_pkg/npm/binaries.py b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binaries.py
similarity index 72%
rename from archivebox/plugins_pkg/npm/binaries.py
rename to packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binaries.py
index dd9e6214..4f44fc4a 100644
--- a/archivebox/plugins_pkg/npm/binaries.py
+++ b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binaries.py
@@ -4,14 +4,19 @@ __package__ = 'plugins_pkg.npm'
 from typing import List
 
 from pydantic import InstanceOf
+from benedict import benedict
 
-from pydantic_pkgr import BinProvider, BinName, BinaryOverrides
+from pydantic_pkgr import BinProvider, Binary, BinName, BinaryOverrides
+
+from abx_plugin_default_binproviders import get_BINPROVIDERS
+
+DEFAULT_BINPROVIDERS = benedict(get_BINPROVIDERS())
+env = DEFAULT_BINPROVIDERS.env
+apt = DEFAULT_BINPROVIDERS.apt
+brew = DEFAULT_BINPROVIDERS.brew
 
 
-from abx.archivebox.base_binary import BaseBinary, env, apt, brew
-
-
-class NodeBinary(BaseBinary):
+class NodeBinary(Binary):
     name: BinName = 'node'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
     
@@ -23,7 +28,7 @@ class NodeBinary(BaseBinary):
 NODE_BINARY = NodeBinary()
 
 
-class NpmBinary(BaseBinary):
+class NpmBinary(Binary):
     name: BinName = 'npm'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
 
@@ -35,7 +40,7 @@ class NpmBinary(BaseBinary):
 NPM_BINARY = NpmBinary()
 
 
-class NpxBinary(BaseBinary):
+class NpxBinary(Binary):
     name: BinName = 'npx'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
     
diff --git a/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binproviders.py b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binproviders.py
new file mode 100644
index 00000000..e0b26a90
--- /dev/null
+++ b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/binproviders.py
@@ -0,0 +1,39 @@
+import os
+from pathlib import Path
+from typing import Optional
+
+from pydantic_pkgr import NpmProvider, PATHStr, BinProviderName
+
+import abx
+
+DEFAULT_LIB_NPM_DIR = Path('/usr/local/share/abx/npm')
+
+OLD_NODE_BIN_PATH = Path(os.getcwd()) / 'node_modules' / '.bin'
+NEW_NODE_BIN_PATH = DEFAULT_LIB_NPM_DIR / 'node_modules' / '.bin'
+
+
+class SystemNpmBinProvider(NpmProvider):
+    name: BinProviderName = "sys_npm"
+    
+    npm_prefix: Optional[Path] = None
+
+
+class LibNpmBinProvider(NpmProvider):
+    name: BinProviderName = "lib_npm"
+    PATH: PATHStr = f'{NEW_NODE_BIN_PATH}:{OLD_NODE_BIN_PATH}'
+    
+    npm_prefix: Optional[Path] = DEFAULT_LIB_NPM_DIR
+    
+    def setup(self) -> None:
+        # update paths from config at runtime
+        LIB_DIR = abx.pm.hook.get_CONFIG().LIB_DIR
+    
+        self.npm_prefix = LIB_DIR / 'npm'
+        self.PATH = f'{LIB_DIR / "npm" / "node_modules" / ".bin"}:{NEW_NODE_BIN_PATH}:{OLD_NODE_BIN_PATH}'
+
+        super().setup()
+
+
+SYS_NPM_BINPROVIDER = SystemNpmBinProvider()
+LIB_NPM_BINPROVIDER = LibNpmBinProvider()
+npm = LIB_NPM_BINPROVIDER
diff --git a/archivebox/plugins_pkg/npm/config.py b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/config.py
similarity index 79%
rename from archivebox/plugins_pkg/npm/config.py
rename to packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/config.py
index f69cfdd2..b937ed27 100644
--- a/archivebox/plugins_pkg/npm/config.py
+++ b/packages/abx-plugin-npm-binprovider/abx_plugin_npm_binprovider/config.py
@@ -1,7 +1,4 @@
-__package__ = 'plugins_pkg.npm'
-
-
-from abx.archivebox.base_configset import BaseConfigSet
+from abx_spec_config import BaseConfigSet
 
 
 ###################### Config ##########################
diff --git a/packages/abx-plugin-npm-binprovider/pyproject.toml b/packages/abx-plugin-npm-binprovider/pyproject.toml
new file mode 100644
index 00000000..5d614f90
--- /dev/null
+++ b/packages/abx-plugin-npm-binprovider/pyproject.toml
@@ -0,0 +1,20 @@
+[project]
+name = "abx-plugin-npm-binprovider"
+version = "2024.10.24"
+description = "NPM binary provider plugin for ABX"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "pydantic-pkgr>=0.5.4",
+    "abx-spec-pydantic-pkgr>=0.1.0",
+    "abx-spec-config>=0.1.0",
+    "abx-plugin-default-binproviders>=2024.10.24",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_plugin_npm_binprovider = "abx_plugin_npm_binprovider"
diff --git a/packages/abx-plugin-pip-binprovider/README.md b/packages/abx-plugin-pip-binprovider/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_pkg/pip/.plugin_order b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/.plugin_order
similarity index 100%
rename from archivebox/plugins_pkg/pip/.plugin_order
rename to packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/.plugin_order
diff --git a/archivebox/plugins_pkg/pip/__init__.py b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/__init__.py
similarity index 62%
rename from archivebox/plugins_pkg/pip/__init__.py
rename to packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/__init__.py
index c1be27b1..8445055f 100644
--- a/archivebox/plugins_pkg/pip/__init__.py
+++ b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/__init__.py
@@ -1,33 +1,19 @@
-__package__ = 'plugins_pkg.pip'
-__label__ = 'pip'
-__version__ = '2024.10.14'
-__author__ = 'ArchiveBox'
-__homepage__ = 'https://github.com/pypa/pip'
+__package__ = 'abx_plugin_pip_binprovider'
+__id__ = 'pip'
+__label__ = 'PIP'
 
 import abx
 
 
-@abx.hookimpl
-def get_PLUGIN():
-    return {
-        'pip': {
-            'PACKAGE': __package__,
-            'LABEL': __label__,
-            'VERSION': __version__,
-            'AUTHOR': __author__,
-            'HOMEPAGE': __homepage__,
-        }
-    }
-
 @abx.hookimpl
 def get_CONFIG():
     from .config import PIP_CONFIG
     
     return {
-        'pip': PIP_CONFIG
+        __id__: PIP_CONFIG
     }
 
-@abx.hookimpl
+@abx.hookimpl(tryfirst=True)
 def get_BINARIES():
     from .binaries import ARCHIVEBOX_BINARY, PYTHON_BINARY, DJANGO_BINARY, SQLITE_BINARY, PIP_BINARY, PIPX_BINARY
     
diff --git a/archivebox/plugins_pkg/pip/binaries.py b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binaries.py
similarity index 84%
rename from archivebox/plugins_pkg/pip/binaries.py
rename to packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binaries.py
index 3e451cfe..b1974250 100644
--- a/archivebox/plugins_pkg/pip/binaries.py
+++ b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binaries.py
@@ -1,4 +1,4 @@
-__package__ = 'plugins_pkg.pip'
+__package__ = 'abx_plugin_pip_binprovider'
 
 import sys
 from pathlib import Path
@@ -9,29 +9,30 @@ from pydantic import InstanceOf, Field, model_validator
 import django
 import django.db.backends.sqlite3.base
 from django.db.backends.sqlite3.base import Database as django_sqlite3     # type: ignore[import-type]
-from pydantic_pkgr import BinProvider, BinName, BinaryOverrides, SemVer
+from pydantic_pkgr import BinProvider, Binary, BinName, BinaryOverrides, SemVer
 
-from archivebox import VERSION
 
-from abx.archivebox.base_binary import BaseBinary, BaseBinProvider, env, apt, brew
-
-from archivebox.misc.logging import hint
-
-from .binproviders import LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER
+from .binproviders import LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, env, apt, brew
 
 ###################### Config ##########################
 
+def get_archivebox_version():
+    try:
+        from archivebox import VERSION
+        return VERSION
+    except Exception:
+        return None
 
 
-class ArchiveboxBinary(BaseBinary):
+class ArchiveboxBinary(Binary):
     name: BinName = 'archivebox'
 
     binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
     overrides: BinaryOverrides = {
-        VENV_PIP_BINPROVIDER.name:  {'packages': [], 'version': VERSION},
-        SYS_PIP_BINPROVIDER.name:   {'packages': [], 'version': VERSION},
-        apt.name:                   {'packages': [], 'version': VERSION},
-        brew.name:                  {'packages': [], 'version': VERSION},
+        VENV_PIP_BINPROVIDER.name:  {'packages': [], 'version': get_archivebox_version},
+        SYS_PIP_BINPROVIDER.name:   {'packages': [], 'version': get_archivebox_version},
+        apt.name:                   {'packages': [], 'version': get_archivebox_version},
+        brew.name:                  {'packages': [], 'version': get_archivebox_version},
     }
     
     # @validate_call
@@ -45,7 +46,7 @@ class ArchiveboxBinary(BaseBinary):
 ARCHIVEBOX_BINARY = ArchiveboxBinary()
 
 
-class PythonBinary(BaseBinary):
+class PythonBinary(Binary):
     name: BinName = 'python'
 
     binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
@@ -71,9 +72,9 @@ LOADED_SQLITE_PATH = Path(django.db.backends.sqlite3.base.__file__)
 LOADED_SQLITE_VERSION = SemVer(django_sqlite3.version)
 LOADED_SQLITE_FROM_VENV = str(LOADED_SQLITE_PATH.absolute().resolve()).startswith(str(VENV_PIP_BINPROVIDER.pip_venv.absolute().resolve()))
 
-class SqliteBinary(BaseBinary):
+class SqliteBinary(Binary):
     name: BinName = 'sqlite'
-    binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
+    binproviders_supported: List[InstanceOf[BinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
     overrides: BinaryOverrides = {
         VENV_PIP_BINPROVIDER.name: {
             "abspath": LOADED_SQLITE_PATH if LOADED_SQLITE_FROM_VENV else None,
@@ -93,10 +94,10 @@ class SqliteBinary(BaseBinary):
             cursor.execute('SELECT JSON(\'{"a": "b"}\')')
         except django_sqlite3.OperationalError as exc:
             print(f'[red][X] Your SQLite3 version is missing the required JSON1 extension: {exc}[/red]')
-            hint([
-                'Upgrade your Python version or install the extension manually:',
-                'https://code.djangoproject.com/wiki/JSON1Extension'
-            ])
+            print(
+                '[violet]Hint:[/violet] Upgrade your Python version or install the extension manually:\n' +
+                '      https://code.djangoproject.com/wiki/JSON1Extension\n'
+            )
         return self
     
     # @validate_call
@@ -114,10 +115,10 @@ LOADED_DJANGO_PATH = Path(django.__file__)
 LOADED_DJANGO_VERSION = SemVer(django.VERSION[:3])
 LOADED_DJANGO_FROM_VENV = str(LOADED_DJANGO_PATH.absolute().resolve()).startswith(str(VENV_PIP_BINPROVIDER.pip_venv and VENV_PIP_BINPROVIDER.pip_venv.absolute().resolve()))
 
-class DjangoBinary(BaseBinary):
+class DjangoBinary(Binary):
     name: BinName = 'django'
 
-    binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
+    binproviders_supported: List[InstanceOf[BinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
     overrides: BinaryOverrides = {
         VENV_PIP_BINPROVIDER.name: {
             "abspath": LOADED_DJANGO_PATH if LOADED_DJANGO_FROM_VENV else None,
@@ -139,7 +140,7 @@ class DjangoBinary(BaseBinary):
 
 DJANGO_BINARY = DjangoBinary()
 
-class PipBinary(BaseBinary):
+class PipBinary(Binary):
     name: BinName = "pip"
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
 
@@ -154,7 +155,7 @@ class PipBinary(BaseBinary):
 PIP_BINARY = PipBinary()
 
 
-class PipxBinary(BaseBinary):
+class PipxBinary(Binary):
     name: BinName = "pipx"
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
 
diff --git a/archivebox/plugins_pkg/pip/binproviders.py b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binproviders.py
similarity index 76%
rename from archivebox/plugins_pkg/pip/binproviders.py
rename to packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binproviders.py
index e51dc780..1c245b62 100644
--- a/archivebox/plugins_pkg/pip/binproviders.py
+++ b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/binproviders.py
@@ -1,21 +1,26 @@
-__package__ = 'plugins_pkg.pip'
-
 import os
 import sys
 import site
 from pathlib import Path
 from typing import Optional
 
+from benedict import benedict
+
 from pydantic_pkgr import PipProvider, BinName, BinProviderName
 
-from archivebox.config import CONSTANTS
+import abx
 
-from abx.archivebox.base_binary import BaseBinProvider
+from abx_plugin_default_binproviders import get_BINPROVIDERS
+
+DEFAULT_BINPROVIDERS = benedict(get_BINPROVIDERS())
+env = DEFAULT_BINPROVIDERS.env
+apt = DEFAULT_BINPROVIDERS.apt
+brew = DEFAULT_BINPROVIDERS.brew
 
 
 ###################### Config ##########################
 
-class SystemPipBinProvider(PipProvider, BaseBinProvider):
+class SystemPipBinProvider(PipProvider):
     name: BinProviderName = "sys_pip"
     INSTALLER_BIN: BinName = "pip"
     
@@ -25,7 +30,7 @@ class SystemPipBinProvider(PipProvider, BaseBinProvider):
         # never modify system pip packages
         return 'refusing to install packages globally with system pip, use a venv instead'
 
-class SystemPipxBinProvider(PipProvider, BaseBinProvider):
+class SystemPipxBinProvider(PipProvider):
     name: BinProviderName = "pipx"
     INSTALLER_BIN: BinName = "pipx"
     
@@ -34,7 +39,7 @@ class SystemPipxBinProvider(PipProvider, BaseBinProvider):
 
 IS_INSIDE_VENV = sys.prefix != sys.base_prefix
 
-class VenvPipBinProvider(PipProvider, BaseBinProvider):
+class VenvPipBinProvider(PipProvider):
     name: BinProviderName = "venv_pip"
     INSTALLER_BIN: BinName = "pip"
 
@@ -45,18 +50,16 @@ class VenvPipBinProvider(PipProvider, BaseBinProvider):
         return None
     
 
-class LibPipBinProvider(PipProvider, BaseBinProvider):
+class LibPipBinProvider(PipProvider):
     name: BinProviderName = "lib_pip"
     INSTALLER_BIN: BinName = "pip"
     
-    pip_venv: Optional[Path] = CONSTANTS.DEFAULT_LIB_DIR / 'pip' / 'venv'
+    pip_venv: Optional[Path] = Path('/usr/local/share/abx/pip/venv')
     
     def setup(self) -> None:
-        # update paths from config if they arent the default
-        from archivebox.config.common import STORAGE_CONFIG
-        if STORAGE_CONFIG.LIB_DIR != CONSTANTS.DEFAULT_LIB_DIR:
-            self.pip_venv = STORAGE_CONFIG.LIB_DIR / 'pip' / 'venv'
-            
+        # update venv path to match most up-to-date LIB_DIR based on runtime config
+        LIB_DIR = abx.pm.hook.get_FLAT_CONFIG().LIB_DIR
+        self.pip_venv = LIB_DIR / 'pip' / 'venv'
         super().setup()
 
 SYS_PIP_BINPROVIDER = SystemPipBinProvider()
diff --git a/archivebox/plugins_pkg/pip/config.py b/packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/config.py
similarity index 100%
rename from archivebox/plugins_pkg/pip/config.py
rename to packages/abx-plugin-pip-binprovider/abx_plugin_pip_binprovider/config.py
diff --git a/packages/abx-plugin-pip-binprovider/pyproject.toml b/packages/abx-plugin-pip-binprovider/pyproject.toml
new file mode 100644
index 00000000..3f6364e0
--- /dev/null
+++ b/packages/abx-plugin-pip-binprovider/pyproject.toml
@@ -0,0 +1,22 @@
+[project]
+name = "abx-plugin-pip-binprovider"
+version = "2024.10.24"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "pydantic-pkgr>=0.5.4",
+    "abx-spec-config>=0.1.0",
+    "abx-spec-pydantic-pkgr>=0.1.0",
+    "abx-plugin-default-binproviders>=2024.10.24",
+    "django>=5.0.0",
+]
+
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_plugin_pip_binprovider = "abx_plugin_pip_binprovider"
diff --git a/packages/abx-plugin-playwright-binprovider/README.md b/packages/abx-plugin-playwright-binprovider/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_pkg/playwright/__init__.py b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/__init__.py
similarity index 56%
rename from archivebox/plugins_pkg/playwright/__init__.py
rename to packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/__init__.py
index 0f66f42c..557f12c0 100644
--- a/archivebox/plugins_pkg/playwright/__init__.py
+++ b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/__init__.py
@@ -1,30 +1,18 @@
-__package__ = 'plugins_pkg.playwright'
-__label__ = 'playwright'
-__version__ = '2024.10.14'
+__package__ = 'abx_plugin_playwright_binprovider'
+__id__ = 'playwright'
+__label__ = 'Playwright'
 __author__ = 'ArchiveBox'
 __homepage__ = 'https://github.com/microsoft/playwright-python'
 
 import abx
 
 
-@abx.hookimpl
-def get_PLUGIN():
-    return {
-        'playwright': {
-            'PACKAGE': __package__,
-            'LABEL': __label__,
-            'VERSION': __version__,
-            'AUTHOR': __author__,
-            'HOMEPAGE': __homepage__,
-        }
-    }
-
 @abx.hookimpl
 def get_CONFIG():
     from .config import PLAYWRIGHT_CONFIG
     
     return {
-        'playwright': PLAYWRIGHT_CONFIG
+        __id__: PLAYWRIGHT_CONFIG
     }
 
 @abx.hookimpl
diff --git a/archivebox/plugins_pkg/playwright/binaries.py b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binaries.py
similarity index 52%
rename from archivebox/plugins_pkg/playwright/binaries.py
rename to packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binaries.py
index 0ef63646..333da054 100644
--- a/archivebox/plugins_pkg/playwright/binaries.py
+++ b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binaries.py
@@ -1,20 +1,18 @@
-__package__ = 'plugins_pkg.playwright'
+__package__ = 'abx_plugin_playwright_binprovider'
 
 from typing import List
 
 from pydantic import InstanceOf
-from pydantic_pkgr import BinName, BinProvider
+from pydantic_pkgr import BinName, BinProvider, Binary
 
-from abx.archivebox.base_binary import BaseBinary, env
 
-from plugins_pkg.pip.binproviders import SYS_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, LIB_PIP_BINPROVIDER
+from abx_plugin_pip_binprovider.binproviders import LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER
+from abx_plugin_default_binproviders import env
 
 from .config import PLAYWRIGHT_CONFIG
 
 
-
-
-class PlaywrightBinary(BaseBinary):
+class PlaywrightBinary(Binary):
     name: BinName = PLAYWRIGHT_CONFIG.PLAYWRIGHT_BINARY
 
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, env]
diff --git a/archivebox/plugins_pkg/playwright/binproviders.py b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binproviders.py
similarity index 90%
rename from archivebox/plugins_pkg/playwright/binproviders.py
rename to packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binproviders.py
index 7d1238d5..8e472988 100644
--- a/archivebox/plugins_pkg/playwright/binproviders.py
+++ b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/binproviders.py
@@ -1,6 +1,7 @@
-__package__ = 'plugins_pkg.playwright'
+__package__ = 'abx_plugin_playwright_binprovider'
 
 import os
+import shutil
 import platform
 from pathlib import Path
 from typing import List, Optional, Dict, ClassVar
@@ -8,6 +9,7 @@ from typing import List, Optional, Dict, ClassVar
 from pydantic import computed_field, Field
 from pydantic_pkgr import (
     BinName,
+    BinProvider,
     BinProviderName,
     BinProviderOverrides,
     InstallArgs,
@@ -18,11 +20,8 @@ from pydantic_pkgr import (
     DEFAULT_ENV_PATH,
 )
 
-from archivebox.config import CONSTANTS
+import abx
 
-from abx.archivebox.base_binary import BaseBinProvider, env
-
-from plugins_pkg.pip.binproviders import SYS_PIP_BINPROVIDER
 
 from .binaries import PLAYWRIGHT_BINARY
 
@@ -31,11 +30,11 @@ MACOS_PLAYWRIGHT_CACHE_DIR: Path = Path("~/Library/Caches/ms-playwright")
 LINUX_PLAYWRIGHT_CACHE_DIR: Path = Path("~/.cache/ms-playwright")
 
 
-class PlaywrightBinProvider(BaseBinProvider):
+class PlaywrightBinProvider(BinProvider):
     name: BinProviderName = "playwright"
     INSTALLER_BIN: BinName = PLAYWRIGHT_BINARY.name
 
-    PATH: PATHStr = f"{CONSTANTS.DEFAULT_LIB_DIR / 'bin'}:{DEFAULT_ENV_PATH}"
+    PATH: PATHStr = f"{Path('/usr/share/abx') / 'bin'}:{DEFAULT_ENV_PATH}"
 
     playwright_browsers_dir: Path = (
         MACOS_PLAYWRIGHT_CACHE_DIR.expanduser()
@@ -59,12 +58,12 @@ class PlaywrightBinProvider(BaseBinProvider):
             return None
 
     def setup(self) -> None:
-        # update paths from config if they arent the default
-        from archivebox.config.common import STORAGE_CONFIG
-        if STORAGE_CONFIG.LIB_DIR != CONSTANTS.DEFAULT_LIB_DIR:
-            self.PATH = f"{STORAGE_CONFIG.LIB_DIR / 'bin'}:{DEFAULT_ENV_PATH}"
+        # update paths from config at runtime
+        LIB_DIR = abx.pm.hook.get_FLAT_CONFIG().LIB_DIR
+        
+        self.PATH = f"{LIB_DIR / 'bin'}:{DEFAULT_ENV_PATH}"
 
-        assert SYS_PIP_BINPROVIDER.INSTALLER_BIN_ABSPATH, "Pip bin provider not initialized"
+        assert shutil.which('pip'), "Pip bin provider not initialized"
 
         if self.playwright_browsers_dir:
             self.playwright_browsers_dir.mkdir(parents=True, exist_ok=True)
diff --git a/archivebox/plugins_pkg/playwright/config.py b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/config.py
similarity index 59%
rename from archivebox/plugins_pkg/playwright/config.py
rename to packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/config.py
index 23f22efc..0c7c6a50 100644
--- a/archivebox/plugins_pkg/playwright/config.py
+++ b/packages/abx-plugin-playwright-binprovider/abx_plugin_playwright_binprovider/config.py
@@ -1,7 +1,4 @@
-__package__ = 'playwright'
-
-from abx.archivebox.base_configset import BaseConfigSet
-
+from abx_spec_config import BaseConfigSet
 
 class PlaywrightConfigs(BaseConfigSet):
     PLAYWRIGHT_BINARY: str = 'playwright'
diff --git a/packages/abx-plugin-playwright-binprovider/pyproject.toml b/packages/abx-plugin-playwright-binprovider/pyproject.toml
new file mode 100644
index 00000000..a6c8937b
--- /dev/null
+++ b/packages/abx-plugin-playwright-binprovider/pyproject.toml
@@ -0,0 +1,20 @@
+[project]
+name = "abx-plugin-playwright-binprovider"
+version = "2024.10.24"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "pydantic>=2.4.2",
+    "pydantic-pkgr>=0.5.4",
+    "abx-spec-pydantic-pkgr>=0.1.0",
+    "abx-spec-config>=0.1.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_plugin_playwright_binprovider = "abx_plugin_playwright_binprovider"
diff --git a/packages/abx-plugin-pocket-extractor/README.md b/packages/abx-plugin-pocket-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/pocket/__init__.py b/packages/abx-plugin-pocket-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/pocket/__init__.py
rename to packages/abx-plugin-pocket-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/pocket/config.py b/packages/abx-plugin-pocket-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/pocket/config.py
rename to packages/abx-plugin-pocket-extractor/config.py
diff --git a/packages/abx-plugin-pocket-extractor/pyproject.toml b/packages/abx-plugin-pocket-extractor/pyproject.toml
new file mode 100644
index 00000000..c9af2450
--- /dev/null
+++ b/packages/abx-plugin-pocket-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-pocket-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-puppeteer-binprovider/README.md b/packages/abx-plugin-puppeteer-binprovider/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_pkg/puppeteer/__init__.py b/packages/abx-plugin-puppeteer-binprovider/__init__.py
similarity index 100%
rename from archivebox/plugins_pkg/puppeteer/__init__.py
rename to packages/abx-plugin-puppeteer-binprovider/__init__.py
diff --git a/archivebox/plugins_pkg/puppeteer/binaries.py b/packages/abx-plugin-puppeteer-binprovider/binaries.py
similarity index 100%
rename from archivebox/plugins_pkg/puppeteer/binaries.py
rename to packages/abx-plugin-puppeteer-binprovider/binaries.py
diff --git a/archivebox/plugins_pkg/puppeteer/binproviders.py b/packages/abx-plugin-puppeteer-binprovider/binproviders.py
similarity index 96%
rename from archivebox/plugins_pkg/puppeteer/binproviders.py
rename to packages/abx-plugin-puppeteer-binprovider/binproviders.py
index 2ef0eb7a..0fa9ca33 100644
--- a/archivebox/plugins_pkg/puppeteer/binproviders.py
+++ b/packages/abx-plugin-puppeteer-binprovider/binproviders.py
@@ -42,7 +42,8 @@ class PuppeteerBinProvider(BaseBinProvider):
     _browser_abspaths: ClassVar[Dict[str, HostBinPath]] = {}
     
     def setup(self) -> None:
-        # update paths from config
+        # update paths from config, don't do this lazily because we dont want to import archivebox.config.common at import-time
+        # we want to avoid depending on archivebox from abx code if at all possible
         from archivebox.config.common import STORAGE_CONFIG
         self.puppeteer_browsers_dir = STORAGE_CONFIG.LIB_DIR / 'browsers'
         self.PATH = str(STORAGE_CONFIG.LIB_DIR / 'bin')
diff --git a/archivebox/plugins_pkg/puppeteer/config.py b/packages/abx-plugin-puppeteer-binprovider/config.py
similarity index 100%
rename from archivebox/plugins_pkg/puppeteer/config.py
rename to packages/abx-plugin-puppeteer-binprovider/config.py
diff --git a/packages/abx-plugin-puppeteer-binprovider/pyproject.toml b/packages/abx-plugin-puppeteer-binprovider/pyproject.toml
new file mode 100644
index 00000000..e901ca88
--- /dev/null
+++ b/packages/abx-plugin-puppeteer-binprovider/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-puppeteer-binprovider"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-readability-extractor/README.md b/packages/abx-plugin-readability-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/readability/__init__.py b/packages/abx-plugin-readability-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/readability/__init__.py
rename to packages/abx-plugin-readability-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/readability/binaries.py b/packages/abx-plugin-readability-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/readability/binaries.py
rename to packages/abx-plugin-readability-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/readability/config.py b/packages/abx-plugin-readability-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/readability/config.py
rename to packages/abx-plugin-readability-extractor/config.py
diff --git a/archivebox/plugins_extractor/readability/extractors.py b/packages/abx-plugin-readability-extractor/extractors.py
similarity index 100%
rename from archivebox/plugins_extractor/readability/extractors.py
rename to packages/abx-plugin-readability-extractor/extractors.py
diff --git a/packages/abx-plugin-readability-extractor/pyproject.toml b/packages/abx-plugin-readability-extractor/pyproject.toml
new file mode 100644
index 00000000..5caa0adb
--- /dev/null
+++ b/packages/abx-plugin-readability-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-readability-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-readwise-extractor/README.md b/packages/abx-plugin-readwise-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/readwise/__init__.py b/packages/abx-plugin-readwise-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/readwise/__init__.py
rename to packages/abx-plugin-readwise-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/readwise/config.py b/packages/abx-plugin-readwise-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/readwise/config.py
rename to packages/abx-plugin-readwise-extractor/config.py
diff --git a/packages/abx-plugin-readwise-extractor/pyproject.toml b/packages/abx-plugin-readwise-extractor/pyproject.toml
new file mode 100644
index 00000000..7df49b56
--- /dev/null
+++ b/packages/abx-plugin-readwise-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-readwise-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-ripgrep-search/README.md b/packages/abx-plugin-ripgrep-search/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_search/ripgrep/__init__.py b/packages/abx-plugin-ripgrep-search/__init__.py
similarity index 100%
rename from archivebox/plugins_search/ripgrep/__init__.py
rename to packages/abx-plugin-ripgrep-search/__init__.py
diff --git a/archivebox/plugins_search/ripgrep/binaries.py b/packages/abx-plugin-ripgrep-search/binaries.py
similarity index 100%
rename from archivebox/plugins_search/ripgrep/binaries.py
rename to packages/abx-plugin-ripgrep-search/binaries.py
diff --git a/archivebox/plugins_search/ripgrep/config.py b/packages/abx-plugin-ripgrep-search/config.py
similarity index 100%
rename from archivebox/plugins_search/ripgrep/config.py
rename to packages/abx-plugin-ripgrep-search/config.py
diff --git a/packages/abx-plugin-ripgrep-search/pyproject.toml b/packages/abx-plugin-ripgrep-search/pyproject.toml
new file mode 100644
index 00000000..c79821d1
--- /dev/null
+++ b/packages/abx-plugin-ripgrep-search/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-ripgrep-search"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_search/ripgrep/searchbackend.py b/packages/abx-plugin-ripgrep-search/searchbackend.py
similarity index 100%
rename from archivebox/plugins_search/ripgrep/searchbackend.py
rename to packages/abx-plugin-ripgrep-search/searchbackend.py
diff --git a/packages/abx-plugin-singlefile-extractor/README.md b/packages/abx-plugin-singlefile-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/singlefile/__init__.py b/packages/abx-plugin-singlefile-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/singlefile/__init__.py
rename to packages/abx-plugin-singlefile-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/singlefile/binaries.py b/packages/abx-plugin-singlefile-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/singlefile/binaries.py
rename to packages/abx-plugin-singlefile-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/singlefile/config.py b/packages/abx-plugin-singlefile-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/singlefile/config.py
rename to packages/abx-plugin-singlefile-extractor/config.py
diff --git a/archivebox/plugins_extractor/singlefile/extractors.py b/packages/abx-plugin-singlefile-extractor/extractors.py
similarity index 100%
rename from archivebox/plugins_extractor/singlefile/extractors.py
rename to packages/abx-plugin-singlefile-extractor/extractors.py
diff --git a/archivebox/plugins_extractor/singlefile/models.py b/packages/abx-plugin-singlefile-extractor/models.py
similarity index 100%
rename from archivebox/plugins_extractor/singlefile/models.py
rename to packages/abx-plugin-singlefile-extractor/models.py
diff --git a/packages/abx-plugin-singlefile-extractor/pyproject.toml b/packages/abx-plugin-singlefile-extractor/pyproject.toml
new file mode 100644
index 00000000..b0c9df1b
--- /dev/null
+++ b/packages/abx-plugin-singlefile-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-singlefile-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-plugin-sonic-search/README.md b/packages/abx-plugin-sonic-search/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_search/sonic/__init__.py b/packages/abx-plugin-sonic-search/__init__.py
similarity index 100%
rename from archivebox/plugins_search/sonic/__init__.py
rename to packages/abx-plugin-sonic-search/__init__.py
diff --git a/archivebox/plugins_search/sonic/binaries.py b/packages/abx-plugin-sonic-search/binaries.py
similarity index 100%
rename from archivebox/plugins_search/sonic/binaries.py
rename to packages/abx-plugin-sonic-search/binaries.py
diff --git a/archivebox/plugins_search/sonic/config.py b/packages/abx-plugin-sonic-search/config.py
similarity index 100%
rename from archivebox/plugins_search/sonic/config.py
rename to packages/abx-plugin-sonic-search/config.py
diff --git a/packages/abx-plugin-sonic-search/pyproject.toml b/packages/abx-plugin-sonic-search/pyproject.toml
new file mode 100644
index 00000000..a61d17c7
--- /dev/null
+++ b/packages/abx-plugin-sonic-search/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-sonic-search"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_search/sonic/searchbackend.py b/packages/abx-plugin-sonic-search/searchbackend.py
similarity index 100%
rename from archivebox/plugins_search/sonic/searchbackend.py
rename to packages/abx-plugin-sonic-search/searchbackend.py
diff --git a/packages/abx-plugin-sqlitefts-search/README.md b/packages/abx-plugin-sqlitefts-search/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_search/sqlitefts/__init__.py b/packages/abx-plugin-sqlitefts-search/__init__.py
similarity index 100%
rename from archivebox/plugins_search/sqlitefts/__init__.py
rename to packages/abx-plugin-sqlitefts-search/__init__.py
diff --git a/archivebox/plugins_search/sqlitefts/config.py b/packages/abx-plugin-sqlitefts-search/config.py
similarity index 100%
rename from archivebox/plugins_search/sqlitefts/config.py
rename to packages/abx-plugin-sqlitefts-search/config.py
diff --git a/packages/abx-plugin-sqlitefts-search/pyproject.toml b/packages/abx-plugin-sqlitefts-search/pyproject.toml
new file mode 100644
index 00000000..f635fb16
--- /dev/null
+++ b/packages/abx-plugin-sqlitefts-search/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-sqlitefts-search"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_search/sqlitefts/searchbackend.py b/packages/abx-plugin-sqlitefts-search/searchbackend.py
similarity index 100%
rename from archivebox/plugins_search/sqlitefts/searchbackend.py
rename to packages/abx-plugin-sqlitefts-search/searchbackend.py
diff --git a/packages/abx-plugin-wget-extractor/README.md b/packages/abx-plugin-wget-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/wget/__init__.py b/packages/abx-plugin-wget-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/wget/__init__.py
rename to packages/abx-plugin-wget-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/wget/binaries.py b/packages/abx-plugin-wget-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/wget/binaries.py
rename to packages/abx-plugin-wget-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/wget/config.py b/packages/abx-plugin-wget-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/wget/config.py
rename to packages/abx-plugin-wget-extractor/config.py
diff --git a/archivebox/plugins_extractor/wget/extractors.py b/packages/abx-plugin-wget-extractor/extractors.py
similarity index 100%
rename from archivebox/plugins_extractor/wget/extractors.py
rename to packages/abx-plugin-wget-extractor/extractors.py
diff --git a/packages/abx-plugin-wget-extractor/pyproject.toml b/packages/abx-plugin-wget-extractor/pyproject.toml
new file mode 100644
index 00000000..21445c18
--- /dev/null
+++ b/packages/abx-plugin-wget-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-wget-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/archivebox/plugins_extractor/wget/wget_util.py b/packages/abx-plugin-wget-extractor/wget_util.py
similarity index 100%
rename from archivebox/plugins_extractor/wget/wget_util.py
rename to packages/abx-plugin-wget-extractor/wget_util.py
diff --git a/packages/abx-plugin-ytdlp-extractor/README.md b/packages/abx-plugin-ytdlp-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/plugins_extractor/ytdlp/__init__.py b/packages/abx-plugin-ytdlp-extractor/__init__.py
similarity index 100%
rename from archivebox/plugins_extractor/ytdlp/__init__.py
rename to packages/abx-plugin-ytdlp-extractor/__init__.py
diff --git a/archivebox/plugins_extractor/ytdlp/binaries.py b/packages/abx-plugin-ytdlp-extractor/binaries.py
similarity index 100%
rename from archivebox/plugins_extractor/ytdlp/binaries.py
rename to packages/abx-plugin-ytdlp-extractor/binaries.py
diff --git a/archivebox/plugins_extractor/ytdlp/config.py b/packages/abx-plugin-ytdlp-extractor/config.py
similarity index 100%
rename from archivebox/plugins_extractor/ytdlp/config.py
rename to packages/abx-plugin-ytdlp-extractor/config.py
diff --git a/packages/abx-plugin-ytdlp-extractor/pyproject.toml b/packages/abx-plugin-ytdlp-extractor/pyproject.toml
new file mode 100644
index 00000000..1b6b4e30
--- /dev/null
+++ b/packages/abx-plugin-ytdlp-extractor/pyproject.toml
@@ -0,0 +1,7 @@
+[project]
+name = "abx-ytdlp-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = []
diff --git a/packages/abx-spec-archivebox/README.md b/packages/abx-spec-archivebox/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/abx-spec-archivebox/abx_spec_archivebox/__init__.py b/packages/abx-spec-archivebox/abx_spec_archivebox/__init__.py
new file mode 100644
index 00000000..5b646bf9
--- /dev/null
+++ b/packages/abx-spec-archivebox/abx_spec_archivebox/__init__.py
@@ -0,0 +1,7 @@
+__package__ = 'abx_spec_archivebox'
+
+# from .effects import *
+# from .events import *
+# from .reads import *
+# from .writes import *
+# from .states import *
diff --git a/archivebox/abx/archivebox/effects.py b/packages/abx-spec-archivebox/abx_spec_archivebox/effects.py
similarity index 100%
rename from archivebox/abx/archivebox/effects.py
rename to packages/abx-spec-archivebox/abx_spec_archivebox/effects.py
diff --git a/archivebox/abx/archivebox/events.py b/packages/abx-spec-archivebox/abx_spec_archivebox/events.py
similarity index 100%
rename from archivebox/abx/archivebox/events.py
rename to packages/abx-spec-archivebox/abx_spec_archivebox/events.py
diff --git a/packages/abx-spec-archivebox/abx_spec_archivebox/reads.py b/packages/abx-spec-archivebox/abx_spec_archivebox/reads.py
new file mode 100644
index 00000000..30d6667d
--- /dev/null
+++ b/packages/abx-spec-archivebox/abx_spec_archivebox/reads.py
@@ -0,0 +1,33 @@
+__package__ = 'abx.archivebox'
+
+
+from benedict import benedict
+
+
+def get_scope_config(defaults: benedict | None = None, persona=None, seed=None, crawl=None, snapshot=None, archiveresult=None, extra_config=None):
+    """Get all the relevant config for the given scope, in correct precedence order"""
+    
+    from django.conf import settings
+    default_config: benedict = defaults or settings.CONFIG
+    
+    snapshot = snapshot or (archiveresult and archiveresult.snapshot)
+    crawl = crawl or (snapshot and snapshot.crawl)
+    seed = seed or (crawl and crawl.seed)
+    persona = persona or (crawl and crawl.persona)
+    
+    persona_config = persona.config if persona else {}
+    seed_config = seed.config if seed else {}
+    crawl_config = crawl.config if crawl else {}
+    snapshot_config = snapshot.config if snapshot else {}
+    archiveresult_config = archiveresult.config if archiveresult else {}
+    extra_config = extra_config or {}
+    
+    return benedict({
+        **default_config,               # defaults / config file / environment variables
+        **persona_config,               # lowest precedence
+        **seed_config,
+        **crawl_config,
+        **snapshot_config,
+        **archiveresult_config,
+        **extra_config,                 # highest precedence
+    })
diff --git a/archivebox/abx/archivebox/states.py b/packages/abx-spec-archivebox/abx_spec_archivebox/states.py
similarity index 100%
rename from archivebox/abx/archivebox/states.py
rename to packages/abx-spec-archivebox/abx_spec_archivebox/states.py
diff --git a/archivebox/abx/archivebox/writes.py b/packages/abx-spec-archivebox/abx_spec_archivebox/writes.py
similarity index 100%
rename from archivebox/abx/archivebox/writes.py
rename to packages/abx-spec-archivebox/abx_spec_archivebox/writes.py
diff --git a/packages/abx-spec-archivebox/pyproject.toml b/packages/abx-spec-archivebox/pyproject.toml
new file mode 100644
index 00000000..349698a7
--- /dev/null
+++ b/packages/abx-spec-archivebox/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "abx-spec-archivebox"
+version = "0.1.0"
+description = "The common shared interfaces for the ABX ArchiveBox plugin ecosystem."
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "django>=5.1.1,<6.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_archivebox = "abx_spec_archivebox"
diff --git a/packages/abx-spec-config/abx_spec_config/__init__.py b/packages/abx-spec-config/abx_spec_config/__init__.py
new file mode 100644
index 00000000..cc840381
--- /dev/null
+++ b/packages/abx-spec-config/abx_spec_config/__init__.py
@@ -0,0 +1,50 @@
+import os
+from pathlib import Path
+from typing import Dict, Any
+
+from benedict import benedict
+
+
+import abx
+
+from .base_configset import BaseConfigSet, ConfigKeyStr
+
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_collection_config_path() -> Path:
+    return Path(os.getcwd()) / "ArchiveBox.conf"
+
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_system_config_path() -> Path:
+    return Path('~/.config/abx/abx.conf').expanduser()
+
+
+@abx.hookspec
+@abx.hookimpl
+def get_CONFIG() -> Dict[abx.PluginId, BaseConfigSet]:
+    """Get the config for a single plugin -> {plugin_id: PluginConfigSet()}"""
+    return {}
+
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_CONFIGS() -> Dict[abx.PluginId, BaseConfigSet]:
+    """Get the config for all plugins by plugin_id -> {plugin_abc: PluginABCConfigSet(), plugin_xyz: PluginXYZConfigSet(), ...}"""
+    return abx.as_dict(abx.pm.hook.get_CONFIG())
+
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_FLAT_CONFIG() -> Dict[ConfigKeyStr, Any]:
+    """Get the flat config assembled from all plugins config -> {SOME_KEY: 'someval', 'OTHER_KEY': 'otherval', ...}"""
+    return benedict({
+        key: value
+        for configset in get_CONFIGS().values()
+            for key, value in benedict(configset).items()
+    })
+
+
+# TODO: add read_config_file(), write_config_file() hooks
diff --git a/archivebox/abx/archivebox/base_configset.py b/packages/abx-spec-config/abx_spec_config/base_configset.py
similarity index 73%
rename from archivebox/abx/archivebox/base_configset.py
rename to packages/abx-spec-config/abx_spec_config/base_configset.py
index 706b9df8..434db331 100644
--- a/archivebox/abx/archivebox/base_configset.py
+++ b/packages/abx-spec-config/abx_spec_config/base_configset.py
@@ -1,36 +1,32 @@
-__package__ = 'abx.archivebox'
+__package__ = 'abx_spec_config'
 
 import os
 import sys
 import re
 from pathlib import Path
 from typing import Type, Tuple, Callable, ClassVar, Dict, Any
+from typing_extensions import Annotated
 
 import toml
 from rich import print
 
 from benedict import benedict
-from pydantic import model_validator, TypeAdapter, AliasChoices
+from pydantic import model_validator, TypeAdapter, AliasChoices, AfterValidator
 from pydantic_settings import BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource
 from pydantic_settings.sources import TomlConfigSettingsSource
 
-from pydantic_pkgr import func_takes_args_or_kwargs
-
+import abx
 
 from . import toml_util
 
 
-PACKAGE_DIR = Path(__file__).resolve().parent.parent
-DATA_DIR = Path(os.getcwd()).resolve()
-
-ARCHIVEBOX_CONFIG_FILE = DATA_DIR / "ArchiveBox.conf"
-ARCHIVEBOX_CONFIG_FILE_BAK = ARCHIVEBOX_CONFIG_FILE.parent / ".ArchiveBox.conf.bak"
-
 AUTOFIXES_HEADER = "[AUTOFIXES]"
 AUTOFIXES_SUBHEADER = "# The following config was added automatically to fix problems detected at startup:"
 
 _ALREADY_WARNED_ABOUT_UPDATED_CONFIG = set()
 
+ConfigKeyStr = Annotated[str, AfterValidator(lambda x: x.isidentifier() and x.isupper() and not x.startswith('_'))]
+
 
 class FlatTomlConfigSettingsSource(TomlConfigSettingsSource):
     """
@@ -98,9 +94,10 @@ class BaseConfigSet(BaseSettings):
         revalidate_instances="subclass-instances",
     )
     
-    load_from_defaults: ClassVar[bool] = True
-    load_from_collection: ClassVar[bool] = True
-    load_from_environment: ClassVar[bool] = True
+    load_from_defaults: ClassVar[bool] = True      # read from schema defaults
+    load_from_system: ClassVar[bool] = True        # read from ~/.config/abx/abx.conf
+    load_from_collection: ClassVar[bool] = True    # read from ./ArchiveBox.conf
+    load_from_environment: ClassVar[bool] = True   # read from environment variables
 
     @classmethod
     def settings_customise_sources(
@@ -115,49 +112,41 @@ class BaseConfigSet(BaseSettings):
         
         # import ipdb; ipdb.set_trace()
         
-        precedence_order = {}
+        default_configs = [init_settings] if cls.load_from_defaults else []
+        system_configs = []
+        collection_configs = []
+        environment_configs = [env_settings] if cls.load_from_environment else []
         
-        # if ArchiveBox.conf does not exist yet, return defaults -> env order
-        if not ARCHIVEBOX_CONFIG_FILE.is_file():
-            precedence_order = {
-                'defaults': init_settings,
-                'environment': env_settings,
-            }
+        # load system config from ~/.config/abx/abx.conf
+        SYSTEM_CONFIG_FILE = abx.pm.hook.get_system_config_path()
+        if cls.load_from_system and os.path.isfile(SYSTEM_CONFIG_FILE):
+            try:
+                system_configs = [FlatTomlConfigSettingsSource(settings_cls, toml_file=SYSTEM_CONFIG_FILE)]
+            except Exception as err:
+                if err.__class__.__name__ == "TOMLDecodeError":
+                    convert_ini_to_toml(SYSTEM_CONFIG_FILE)
+                    system_configs = [FlatTomlConfigSettingsSource(settings_cls, toml_file=SYSTEM_CONFIG_FILE)]
+                else:
+                    raise
+                
+        COLLECTION_CONFIG_FILE = abx.pm.hook.get_collection_config_path()
+        if cls.load_from_collection and os.path.isfile(COLLECTION_CONFIG_FILE):
+            try:
+                collection_configs = [FlatTomlConfigSettingsSource(settings_cls, toml_file=COLLECTION_CONFIG_FILE)]
+            except Exception as err:
+                if err.__class__.__name__ == "TOMLDecodeError":
+                    convert_ini_to_toml(COLLECTION_CONFIG_FILE)
+                    collection_configs = [FlatTomlConfigSettingsSource(settings_cls, toml_file=COLLECTION_CONFIG_FILE)]
+                else:
+                    raise
         
-        # if ArchiveBox.conf exists and is in TOML format, return default -> TOML -> env order
-        try:
-            precedence_order = precedence_order or {
-                'defaults': init_settings,
-                # 'collection': FlatTomlConfigSettingsSource(settings_cls, toml_file=ARCHIVEBOX_CONFIG_FILE),
-                'collection': FlatTomlConfigSettingsSource(settings_cls, toml_file=ARCHIVEBOX_CONFIG_FILE),
-                'environment': env_settings,
-            }
-        except Exception as err:
-            if err.__class__.__name__ != "TOMLDecodeError":
-                raise
-            # if ArchiveBox.conf exists and is in INI format, convert it then return default -> TOML -> env order
-
-            # Convert ArchiveBox.conf in INI format to TOML and save original to .ArchiveBox.bak
-            original_ini = ARCHIVEBOX_CONFIG_FILE.read_text()
-            ARCHIVEBOX_CONFIG_FILE_BAK.write_text(original_ini)
-            new_toml = toml_util.convert(original_ini)
-            ARCHIVEBOX_CONFIG_FILE.write_text(new_toml)
-
-            precedence_order = {
-                'defaults': init_settings,
-                # 'collection': FlatTomlConfigSettingsSource(settings_cls, toml_file=ARCHIVEBOX_CONFIG_FILE),
-                'collection': FlatTomlConfigSettingsSource(settings_cls, toml_file=ARCHIVEBOX_CONFIG_FILE),
-                'environment': env_settings,
-            }
-            
-        if not cls.load_from_environment:
-            precedence_order.pop('environment')
-        if not cls.load_from_collection:
-            precedence_order.pop('collection')
-        if not cls.load_from_defaults:
-            precedence_order.pop('defaults')
-
-        return tuple(precedence_order.values())
+        precedence_order = [
+            *default_configs,
+            *system_configs,
+            *collection_configs,
+            *environment_configs,
+        ]
+        return tuple(precedence_order)
 
     @model_validator(mode="after")
     def fill_defaults(self):
@@ -175,7 +164,7 @@ class BaseConfigSet(BaseSettings):
         """Manual validation method, to be called from plugin/__init__.py:get_CONFIG()"""
         pass
     
-    def get_default_value(self, key):
+    def get_default_value(self, key: ConfigKeyStr):
         """Get the default value for a given config key"""
         field = self.model_fields[key]
         value = getattr(self, key)
@@ -204,7 +193,9 @@ class BaseConfigSet(BaseSettings):
         Example acceptable use case: user config says SEARCH_BACKEND_ENGINE=sonic but sonic_client pip library is not installed so we cannot use it.
         SEARCH_BACKEND_CONFIG.update_in_place(SEARCH_BACKEND_ENGINE='ripgrep') can be used to reset it back to ripgrep so we can continue.
         """
-        from archivebox.misc.toml_util import CustomTOMLEncoder
+        
+        COLLECTION_CONFIG_FILE = abx.pm.hook.get_collection_config_path()
+        # SYSTEM_CONFIG_FILE = abx.pm.hook.get_system_config_path()
         
         # silence warnings if they've already been shown once
         if all(key in _ALREADY_WARNED_ABOUT_UPDATED_CONFIG for key in kwargs.keys()):
@@ -224,10 +215,10 @@ class BaseConfigSet(BaseSettings):
         
         # if persist=True, write config changes to data/ArchiveBox.conf [AUTOFIXES] section
         try:
-            if persist and ARCHIVEBOX_CONFIG_FILE.is_file():
-                autofixes_to_add = benedict(kwargs).to_toml(encoder=CustomTOMLEncoder())
+            if persist and COLLECTION_CONFIG_FILE.is_file():
+                autofixes_to_add = benedict(kwargs).to_toml(encoder=toml_util.CustomTOMLEncoder())
                 
-                existing_config = ARCHIVEBOX_CONFIG_FILE.read_text().split(AUTOFIXES_HEADER, 1)[0].strip()
+                existing_config = COLLECTION_CONFIG_FILE.read_text().split(AUTOFIXES_HEADER, 1)[0].strip()
                 if AUTOFIXES_HEADER in existing_config:
                     existing_autofixes = existing_config.split(AUTOFIXES_HEADER, 1)[-1].strip().replace(AUTOFIXES_SUBHEADER, '').replace(AUTOFIXES_HEADER, '').strip()
                 else:
@@ -240,7 +231,7 @@ class BaseConfigSet(BaseSettings):
                     existing_autofixes,
                     autofixes_to_add,
                 ] if line.strip()).strip() + '\n'
-                ARCHIVEBOX_CONFIG_FILE.write_text(new_config)
+                COLLECTION_CONFIG_FILE.write_text(new_config)
         except Exception:
             pass
         self.__init__()
@@ -250,7 +241,7 @@ class BaseConfigSet(BaseSettings):
         return self
     
     @property
-    def aliases(self) -> Dict[str, str]:
+    def aliases(self) -> Dict[ConfigKeyStr, ConfigKeyStr]:
         alias_map = {}
         for key, field in self.model_fields.items():
             alias_map[key] = key
@@ -276,7 +267,7 @@ class BaseConfigSet(BaseSettings):
         return re.sub('([A-Z]+)', r'_\1', class_name).upper().strip('_')
     
     
-    def from_defaults(self) -> Dict[str, Any]:
+    def from_defaults(self) -> Dict[ConfigKeyStr, Any]:
         """Get the dictionary of {key: value} config loaded from the default values"""
         class OnlyDefaultsConfig(self.__class__):
             load_from_defaults = True
@@ -284,7 +275,7 @@ class BaseConfigSet(BaseSettings):
             load_from_environment = False
         return benedict(OnlyDefaultsConfig().model_dump(exclude_unset=False, exclude_defaults=False, exclude=set(self.model_computed_fields.keys())))
     
-    def from_collection(self) -> Dict[str, Any]:
+    def from_collection(self) -> Dict[ConfigKeyStr, Any]:
         """Get the dictionary of {key: value} config loaded from the collection ArchiveBox.conf"""
         class OnlyConfigFileConfig(self.__class__):
             load_from_defaults = False
@@ -292,7 +283,7 @@ class BaseConfigSet(BaseSettings):
             load_from_environment = False
         return benedict(OnlyConfigFileConfig().model_dump(exclude_unset=True, exclude_defaults=False, exclude=set(self.model_computed_fields.keys())))
     
-    def from_environment(self) -> Dict[str, Any]:
+    def from_environment(self) -> Dict[ConfigKeyStr, Any]:
         """Get the dictionary of {key: value} config loaded from the environment variables"""
         class OnlyEnvironmentConfig(self.__class__):
             load_from_defaults = False
@@ -300,12 +291,12 @@ class BaseConfigSet(BaseSettings):
             load_from_environment = True
         return benedict(OnlyEnvironmentConfig().model_dump(exclude_unset=True, exclude_defaults=False, exclude=set(self.model_computed_fields.keys())))
     
-    def from_computed(self) -> Dict[str, Any]:
+    def from_computed(self) -> Dict[ConfigKeyStr, Any]:
         """Get the dictionary of {key: value} config loaded from the computed fields"""
         return benedict(self.model_dump(include=set(self.model_computed_fields.keys())))
     
 
-    def to_toml_dict(self, defaults=False) -> Dict[str, Any]:
+    def to_toml_dict(self, defaults=False) -> Dict[ConfigKeyStr, Any]:
         """Get the current config as a TOML-ready dict"""
         config_dict = {}
         for key, value in benedict(self).items():
@@ -325,10 +316,24 @@ class BaseConfigSet(BaseSettings):
         
         return toml.dumps(toml_dict, encoder=CustomTOMLEncoder())
     
-    def as_legacy_config_schema(self) -> Dict[str, Any]:
-        # shim for backwards compatibility with old config schema style
-        model_values = self.model_dump()
-        return benedict({
-            key: {'type': field.annotation, 'default': model_values[key]}
-            for key, field in self.model_fields.items()
-        })
+
+
+def func_takes_args_or_kwargs(lambda_func: Callable[..., Any]) -> bool:
+    """returns True if a lambda func takes args/kwargs of any kind, otherwise false if it's pure/argless"""
+    code = lambda_func.__code__
+    has_args = code.co_argcount > 0
+    has_varargs = code.co_flags & 0x04 != 0
+    has_varkw = code.co_flags & 0x08 != 0
+    return has_args or has_varargs or has_varkw
+
+
+
+
+def convert_ini_to_toml(ini_file: Path):
+    """Convert an INI file to a TOML file, saving the original to .ORIGINALNAME.bak"""
+    
+    bak_path = ini_file.parent / f'.{ini_file.name}.bak'
+    original_ini = ini_file.read_text()
+    bak_path.write_text(original_ini)
+    new_toml = toml_util.convert(original_ini)
+    ini_file.write_text(new_toml)
diff --git a/archivebox/abx/archivebox/toml_util.py b/packages/abx-spec-config/abx_spec_config/toml_util.py
similarity index 100%
rename from archivebox/abx/archivebox/toml_util.py
rename to packages/abx-spec-config/abx_spec_config/toml_util.py
diff --git a/packages/abx-spec-config/pyproject.toml b/packages/abx-spec-config/pyproject.toml
new file mode 100644
index 00000000..b85f675e
--- /dev/null
+++ b/packages/abx-spec-config/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "abx-spec-config"
+version = "0.0.1"
+dependencies = [
+    "abx>=0.1.0",
+    "python-benedict>=0.34.0",
+    "pydantic>=2.9.2",
+    "pydantic-settings>=2.6.0",
+    "rich>=13.9.3",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_config = "abx_spec_config"
diff --git a/packages/abx-spec-django/README.md b/packages/abx-spec-django/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/abx/django/hookspec.py b/packages/abx-spec-django/abx_spec_django/__init__.py
similarity index 79%
rename from archivebox/abx/django/hookspec.py
rename to packages/abx-spec-django/abx_spec_django/__init__.py
index 87f8e520..20f62d2b 100644
--- a/archivebox/abx/django/hookspec.py
+++ b/packages/abx-spec-django/abx_spec_django/__init__.py
@@ -1,17 +1,16 @@
-__package__ = 'abx.django'
-
-from ..hookspec import hookspec
-
+import abx
 
 ###########################################################################################
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_INSTALLED_APPS():
     """Return a list of apps to add to INSTALLED_APPS"""
     # e.g. ['your_plugin_type.plugin_name']
-    return []
+    return ['abx_spec_django']
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_INSTALLED_APPS(INSTALLED_APPS):
 #     """Mutate INSTALLED_APPS in place to add your app in a specific position"""
 #     # idx_of_contrib = INSTALLED_APPS.index('django.contrib.auth')
@@ -19,72 +18,85 @@ def get_INSTALLED_APPS():
 #     pass
 
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_TEMPLATE_DIRS():
     return []     # e.g. ['your_plugin_type/plugin_name/templates']
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_TEMPLATE_DIRS(TEMPLATE_DIRS):
 #     """Install django settings"""
 #     # e.g. TEMPLATE_DIRS.insert(0, 'your_plugin_type/plugin_name/templates')
 #     pass
 
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_STATICFILES_DIRS():
     return []     # e.g. ['your_plugin_type/plugin_name/static']
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_STATICFILES_DIRS(STATICFILES_DIRS):
 #     """Mutate STATICFILES_DIRS in place to add your static dirs in a specific position"""
 #     # e.g. STATICFILES_DIRS.insert(0, 'your_plugin_type/plugin_name/static')
 #     pass
 
 
-@hookspec
-def get_MIDDLEWARE():
+@abx.hookspec
+@abx.hookimpl
+def get_MIDDLEWARES():
     return []     # e.g. ['your_plugin_type.plugin_name.middleware.YourMiddleware']
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_MIDDLEWARE(MIDDLEWARE):
 #     """Mutate MIDDLEWARE in place to add your middleware in a specific position"""
 #     # e.g. MIDDLEWARE.insert(0, 'your_plugin_type.plugin_name.middleware.YourMiddleware')
 #     pass
 
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_AUTHENTICATION_BACKENDS():
     return []     # e.g. ['django_auth_ldap.backend.LDAPBackend']
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_AUTHENTICATION_BACKENDS(AUTHENTICATION_BACKENDS):
 #     """Mutate AUTHENTICATION_BACKENDS in place to add your auth backends in a specific position"""
 #     # e.g. AUTHENTICATION_BACKENDS.insert(0, 'your_plugin_type.plugin_name.backend.YourBackend')
 #     pass
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_DJANGO_HUEY_QUEUES(QUEUE_DATABASE_NAME):
-    return []     # e.g. [{'name': 'your_plugin_type.plugin_name', 'HUEY': {...}}]
+    return {}     # e.g. {'some_queue_name': {'filename': 'some_queue_name.sqlite3', 'store_none': True, 'results': True, ...}}
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_DJANGO_HUEY(DJANGO_HUEY):
 #     """Mutate DJANGO_HUEY in place to add your huey queues in a specific position"""
 #     # e.g. DJANGO_HUEY['queues']['some_queue_name']['some_setting'] = 'some_value'
 #     pass
 
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_ADMIN_DATA_VIEWS_URLS():
     return []
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_ADMIN_DATA_VIEWS(ADMIN_DATA_VIEWS):
 #     """Mutate ADMIN_DATA_VIEWS in place to add your admin data views in a specific position"""
 #     # e.g. ADMIN_DATA_VIEWS['URLS'].insert(0, 'your_plugin_type/plugin_name/admin_data_views.py')
 #     pass
 
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_settings(settings):
 #     """Mutate settings in place to add your settings / modify existing settings"""
 #     # settings.SOME_KEY = 'some_value'
@@ -93,11 +105,13 @@ def get_ADMIN_DATA_VIEWS_URLS():
 
 ###########################################################################################
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def get_urlpatterns():
     return []     # e.g. [path('your_plugin_type/plugin_name/url.py', your_view)]
 
-# @hookspec
+# @abx.hookspec
+# @abx.hookimpl
 # def register_urlpatterns(urlpatterns):
 #     """Mutate urlpatterns in place to add your urlpatterns in a specific position"""
 #     # e.g. urlpatterns.insert(0, path('your_plugin_type/plugin_name/url.py', your_view))
@@ -105,21 +119,22 @@ def get_urlpatterns():
 
 ###########################################################################################
 
-@hookspec
-def register_checks():
-    """Register django checks with django system checks system"""
-    pass
 
-@hookspec
+
+@abx.hookspec
+@abx.hookimpl
 def register_admin(admin_site):
     """Register django admin views/models with the main django admin site instance"""
+    # e.g. admin_site.register(your_model, your_admin_class)
     pass
 
 
 ###########################################################################################
 
 
-@hookspec
+@abx.hookspec
+@abx.hookimpl
 def ready():
     """Called when Django apps app.ready() are triggered"""
+    # e.g. abx.pm.hook.get_CONFIG().ytdlp.validate()
     pass
diff --git a/archivebox/abx/django/apps.py b/packages/abx-spec-django/abx_spec_django/apps.py
similarity index 71%
rename from archivebox/abx/django/apps.py
rename to packages/abx-spec-django/abx_spec_django/apps.py
index 085647c1..667b74c0 100644
--- a/archivebox/abx/django/apps.py
+++ b/packages/abx-spec-django/abx_spec_django/apps.py
@@ -1,13 +1,14 @@
-__package__ = 'abx.django'
+__package__ = 'abx_spec_django'
 
 from django.apps import AppConfig
 
+import abx
+
 
 class ABXConfig(AppConfig):
-    name = 'abx'
+    name = 'abx_spec_django'
 
     def ready(self):
-        import abx
         from django.conf import settings
         
         abx.pm.hook.ready(settings=settings)
diff --git a/packages/abx-spec-django/pyproject.toml b/packages/abx-spec-django/pyproject.toml
new file mode 100644
index 00000000..09ed31ff
--- /dev/null
+++ b/packages/abx-spec-django/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "abx-spec-django"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "django>=5.1.1,<6.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_django = "abx_spec_django"
diff --git a/packages/abx-spec-extractor/README.md b/packages/abx-spec-extractor/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/abx-spec-extractor/abx_spec_extractor.py b/packages/abx-spec-extractor/abx_spec_extractor.py
new file mode 100644
index 00000000..74659467
--- /dev/null
+++ b/packages/abx-spec-extractor/abx_spec_extractor.py
@@ -0,0 +1,211 @@
+import os
+
+from typing import Optional, List, Annotated, Tuple
+from pathlib import Path
+
+from pydantic import AfterValidator
+from pydantic_pkgr import BinName
+
+
+import abx
+
+
+def assert_no_empty_args(args: List[str]) -> List[str]:
+    assert all(len(arg) for arg in args)
+    return args
+
+ExtractorName = Annotated[str, AfterValidator(lambda s: s.isidentifier())]
+
+HandlerFuncStr = Annotated[str, AfterValidator(lambda s: s.startswith('self.'))]
+CmdArgsList = Annotated[List[str] | Tuple[str, ...], AfterValidator(assert_no_empty_args)]
+
+
+@abx.hookspec
+@abx.hookimpl
+def get_EXTRACTORS():
+    return []
+
+@abx.hookspec
+@abx.hookimpl
+def extract(uri: str, config: dict | None=None):
+    return {}
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl(trylast=True)
+def should_extract(uri: str, extractor: str, config: dict | None=None):
+    return False
+
+
+class BaseExtractor:
+    name: ExtractorName
+    binary: BinName
+
+    default_args: CmdArgsList = []
+    extra_args: CmdArgsList = []
+
+    def get_output_path(self, snapshot) -> Path:
+        return Path(self.__class__.__name__.lower())
+
+    def should_extract(self, uri: str, config: dict | None=None) -> bool:
+        try:
+            assert self.detect_installed_binary().version
+        except Exception:
+            raise
+            # could not load binary
+            return False
+        
+        # output_dir = self.get_output_path(snapshot)
+        # if output_dir.glob('*.*'):
+        #     return False
+        return True
+
+    # @abx.hookimpl
+    # def extract(self, snapshot_id: str) -> Dict[str, Any]:
+    #     from core.models import Snapshot
+    #     from archivebox import CONSTANTS
+        
+    #     snapshot = Snapshot.objects.get(id=snapshot_id)
+        
+    #     if not self.should_extract(snapshot.url):
+    #         return {}
+        
+    #     status = 'failed'
+    #     start_ts = timezone.now()
+    #     uplink = self.detect_network_interface()
+    #     installed_binary = self.detect_installed_binary()
+    #     machine = installed_binary.machine
+    #     assert uplink.machine == installed_binary.machine  # it would be *very* weird if this wasn't true
+        
+    #     output_dir = CONSTANTS.DATA_DIR / '.tmp' / 'extractors' / self.name / str(snapshot.abid)
+    #     output_dir.mkdir(parents=True, exist_ok=True)
+
+    #     # execute the extractor binary with the given args
+    #     args = [snapshot.url, *self.args] if self.args is not None else [snapshot.url, *self.default_args, *self.extra_args]
+    #     cmd = [str(installed_binary.abspath), *args]
+    #     proc = self.exec(installed_binary=installed_binary, args=args, cwd=output_dir)
+
+    #     # collect the output
+    #     end_ts = timezone.now()
+    #     output_files = list(str(path.relative_to(output_dir)) for path in output_dir.glob('**/*.*'))
+    #     stdout = proc.stdout.strip()
+    #     stderr = proc.stderr.strip()
+    #     output_json = None
+    #     output_text = stdout
+    #     try:
+    #         output_json = json.loads(stdout.strip())
+    #         output_text = None
+    #     except json.JSONDecodeError:
+    #         pass
+        
+    #     errors = []
+    #     if proc.returncode == 0:
+    #         status = 'success'
+    #     else:
+    #         errors.append(f'{installed_binary.name} returned non-zero exit code: {proc.returncode}')   
+
+    #     # increment health stats counters
+    #     if status == 'success':
+    #         machine.record_health_success()
+    #         uplink.record_health_success()
+    #         installed_binary.record_health_success()
+    #     else:
+    #         machine.record_health_failure()
+    #         uplink.record_health_failure()
+    #         installed_binary.record_health_failure()
+
+    #     return {
+    #         'extractor': self.name,
+            
+    #         'snapshot': {
+    #             'id': snapshot.id,
+    #             'abid': snapshot.abid,
+    #             'url': snapshot.url,
+    #             'created_by_id': snapshot.created_by_id,
+    #         },
+            
+    #         'machine': {
+    #             'id': machine.id,
+    #             'abid': machine.abid,
+    #             'guid': machine.guid,
+    #             'hostname': machine.hostname,
+    #             'hw_in_docker': machine.hw_in_docker,
+    #             'hw_in_vm': machine.hw_in_vm,
+    #             'hw_manufacturer': machine.hw_manufacturer,
+    #             'hw_product': machine.hw_product,
+    #             'hw_uuid': machine.hw_uuid,
+    #             'os_arch': machine.os_arch,
+    #             'os_family': machine.os_family,
+    #             'os_platform': machine.os_platform,
+    #             'os_release': machine.os_release,
+    #             'os_kernel': machine.os_kernel,
+    #         },
+            
+    #         'uplink': { 
+    #             'id': uplink.id,
+    #             'abid': uplink.abid,
+    #             'mac_address': uplink.mac_address,
+    #             'ip_public': uplink.ip_public,
+    #             'ip_local': uplink.ip_local,
+    #             'dns_server': uplink.dns_server,
+    #             'hostname': uplink.hostname,
+    #             'iface': uplink.iface,
+    #             'isp': uplink.isp,
+    #             'city': uplink.city,
+    #             'region': uplink.region,
+    #             'country': uplink.country,
+    #         },
+            
+    #         'binary': {
+    #             'id': installed_binary.id,
+    #             'abid': installed_binary.abid,
+    #             'name': installed_binary.name,
+    #             'binprovider': installed_binary.binprovider,
+    #             'abspath': installed_binary.abspath,
+    #             'version': installed_binary.version,
+    #             'sha256': installed_binary.sha256,
+    #         },
+
+    #         'cmd': cmd,
+    #         'stdout': stdout,
+    #         'stderr': stderr,
+    #         'returncode': proc.returncode,
+    #         'start_ts': start_ts,
+    #         'end_ts': end_ts,
+            
+    #         'status': status,
+    #         'errors': errors,
+    #         'output_dir': str(output_dir.relative_to(CONSTANTS.DATA_DIR)),
+    #         'output_files': output_files,
+    #         'output_json': output_json or {},
+    #         'output_text': output_text or '',
+    #     }
+
+    # TODO: move this to a hookimpl
+    def exec(self, args: CmdArgsList=(), cwd: Optional[Path]=None, installed_binary=None):
+        cwd = cwd or Path(os.getcwd())
+        binary = self.load_binary(installed_binary=installed_binary)
+        
+        return binary.exec(cmd=args, cwd=cwd)
+    
+    # @cached_property
+    @property
+    def BINARY(self):
+        # import abx.archivebox.reads
+        # for binary in abx.archivebox.reads.get_BINARIES().values():
+        #     if binary.name == self.binary:
+        #         return binary
+        raise ValueError(f'Binary {self.binary} not found')
+    
+    def detect_installed_binary(self):
+        from machine.models import InstalledBinary
+        # hydrates binary from DB/cache if record of installed version is recent enough
+        # otherwise it finds it from scratch by detecting installed version/abspath/sha256 on host
+        return InstalledBinary.objects.get_from_db_or_cache(self.BINARY)
+
+    def load_binary(self, installed_binary=None):
+        installed_binary = installed_binary or self.detect_installed_binary()
+        return installed_binary.load_from_db()
+    
+    # def detect_network_interface(self):
+    #     from machine.models import NetworkInterface
+    #     return NetworkInterface.objects.current()
diff --git a/packages/abx-spec-extractor/pyproject.toml b/packages/abx-spec-extractor/pyproject.toml
new file mode 100644
index 00000000..5d49fef2
--- /dev/null
+++ b/packages/abx-spec-extractor/pyproject.toml
@@ -0,0 +1,18 @@
+[project]
+name = "abx-spec-extractor"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "python-benedict>=0.26.0",
+    "pydantic>=2.5.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_extractor = "abx_spec_extractor"
diff --git a/packages/abx-spec-pydantic-pkgr/README.md b/packages/abx-spec-pydantic-pkgr/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/abx-spec-pydantic-pkgr/abx_spec_pydantic_pkgr.py b/packages/abx-spec-pydantic-pkgr/abx_spec_pydantic_pkgr.py
new file mode 100644
index 00000000..4665452a
--- /dev/null
+++ b/packages/abx-spec-pydantic-pkgr/abx_spec_pydantic_pkgr.py
@@ -0,0 +1,72 @@
+import os
+
+from typing import Dict
+from pathlib import Path
+
+import abx
+
+from pydantic_pkgr import Binary, BinProvider
+
+###########################################################################################
+
+@abx.hookspec
+@abx.hookimpl()
+def get_BINPROVIDERS() -> Dict[str, BinProvider]:
+    return {}
+
+@abx.hookspec
+@abx.hookimpl()
+def get_BINARIES() -> Dict[str, Binary]:
+    return {}
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_BINPROVIDER(binprovider_name: str) -> BinProvider:
+    return abx.as_dict(abx.pm.hook.get_BINPROVIDERS())[binprovider_name]
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def get_BINARY(bin_name: str) -> BinProvider:
+    return abx.as_dict(abx.pm.hook.get_BINARYS())[bin_name]
+
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def binary_load(binary: Binary, **kwargs) -> Binary:
+    loaded_binary = binary.load(**kwargs)
+    abx.pm.hook.binary_symlink_to_bin_dir(binary=loaded_binary)
+    return loaded_binary
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def binary_install(binary: Binary, **kwargs) -> Binary:
+    loaded_binary = binary.install(**kwargs)
+    abx.pm.hook.binary_symlink_to_bin_dir(binary=loaded_binary)
+    return loaded_binary
+    
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def binary_load_or_install(binary: Binary, **kwargs) -> Binary:
+    loaded_binary = binary.load_or_install(**kwargs)
+    abx.pm.hook.binary_symlink_to_bin_dir(binary=loaded_binary)
+    return loaded_binary
+
+@abx.hookspec(firstresult=True)
+@abx.hookimpl
+def binary_symlink_to_bin_dir(binary: Binary, bin_dir: Path | None=None):
+    LIB_DIR = Path(abx.pm.hook.get_CONFIG().get('LIB_DIR', '/usr/local/share/abx'))
+    BIN_DIR = bin_dir or Path(abx.pm.hook.get_CONFIG().get('BIN_DIR', LIB_DIR / 'bin'))
+            
+    if not (binary.abspath and os.path.isfile(binary.abspath)):
+        return
+            
+    try:
+        BIN_DIR.mkdir(parents=True, exist_ok=True)
+        symlink = BIN_DIR / binary.name
+        symlink.unlink(missing_ok=True)
+        symlink.symlink_to(binary.abspath)
+        symlink.chmod(0o777)   # make sure its executable by everyone
+    except Exception:
+        # print(f'[red]:warning: Failed to symlink {symlink} -> {binary.abspath}[/red] {err}')
+        # not actually needed, we can just run without it
+        pass
diff --git a/packages/abx-spec-pydantic-pkgr/pyproject.toml b/packages/abx-spec-pydantic-pkgr/pyproject.toml
new file mode 100644
index 00000000..67f1f62f
--- /dev/null
+++ b/packages/abx-spec-pydantic-pkgr/pyproject.toml
@@ -0,0 +1,17 @@
+[project]
+name = "abx-spec-pydantic-pkgr"
+version = "0.1.0"
+description = "The ABX plugin specification for Binaries and BinProviders"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "pydantic-pkgr>=0.5.4",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_pydantic_pkgr = "abx_spec_pydantic_pkgr"
diff --git a/packages/abx-spec-searchbackend/README.md b/packages/abx-spec-searchbackend/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/archivebox/abx/archivebox/base_searchbackend.py b/packages/abx-spec-searchbackend/abx_spec_searchbackend.py
similarity index 73%
rename from archivebox/abx/archivebox/base_searchbackend.py
rename to packages/abx-spec-searchbackend/abx_spec_searchbackend.py
index 72713ab8..66b34114 100644
--- a/archivebox/abx/archivebox/base_searchbackend.py
+++ b/packages/abx-spec-searchbackend/abx_spec_searchbackend.py
@@ -1,8 +1,12 @@
-__package__ = 'abx.archivebox'
-
-from typing import Iterable, List
 import abc
+from typing import Iterable, List, Dict
 
+import abx
+
+@abx.hookspec
+@abx.hookimpl
+def get_SEARCHBACKENDS() -> Dict[abx.PluginId, 'BaseSearchBackend']:
+    return {}
 
 
 class BaseSearchBackend(abc.ABC):
diff --git a/packages/abx-spec-searchbackend/pyproject.toml b/packages/abx-spec-searchbackend/pyproject.toml
new file mode 100644
index 00000000..2a9ac3ce
--- /dev/null
+++ b/packages/abx-spec-searchbackend/pyproject.toml
@@ -0,0 +1,18 @@
+[project]
+name = "abx-spec-searchbackend"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "abx>=0.1.0",
+    "python-benedict>=0.26.0",
+    "pydantic>=2.5.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project.entry-points.abx]
+abx_spec_searchbackend = "abx_spec_searchbackend"
diff --git a/packages/abx/README.md b/packages/abx/README.md
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/abx/abx.py b/packages/abx/abx.py
new file mode 100644
index 00000000..0ce28462
--- /dev/null
+++ b/packages/abx/abx.py
@@ -0,0 +1,344 @@
+__package__ = 'abx'
+__id__ = 'abx'
+__label__ = 'ABX'
+__author__ = 'Nick Sweeting'
+__homepage__ = 'https://github.com/ArchiveBox'
+__order__ = 0
+
+
+import sys
+import inspect
+import importlib
+import itertools
+from pathlib import Path
+from typing import Dict, Callable, List, Set, Tuple, Iterable, Any, TypedDict, Type, cast
+from types import ModuleType
+from typing_extensions import Annotated
+from functools import cache
+
+from benedict import benedict
+from pydantic import AfterValidator
+
+from pluggy import HookspecMarker, HookimplMarker, PluginManager, HookimplOpts
+
+spec = hookspec = HookspecMarker("abx")
+impl = hookimpl = HookimplMarker("abx")
+
+
+
+AttrName = Annotated[str, AfterValidator(lambda x: x.isidentifier() and not x.startswith('_'))]
+PluginId = Annotated[str, AfterValidator(lambda x: x.isidentifier() and not x.startswith('_') and x.islower())]
+
+class PluginInfo(TypedDict, total=False):
+    id: PluginId
+    package: AttrName
+    label: str
+    version: str
+    author: str
+    homepage: str
+    dependencies: List[str]
+    
+    source_code: str
+    hooks: Dict[AttrName, Callable]
+    module: ModuleType
+
+
+
+class PatchedPluginManager(PluginManager):
+    """
+    Patch to fix pluggy's PluginManager to work with pydantic models.
+    See: https://github.com/pytest-dev/pluggy/pull/536
+    """
+    def parse_hookimpl_opts(self, plugin, name: str) -> HookimplOpts | None:
+        # IMPORTANT: @property methods can have side effects, and are never hookimpl
+        # if attr is a property, skip it in advance
+        plugin_class = plugin if inspect.isclass(plugin) else type(plugin)
+        if isinstance(getattr(plugin_class, name, None), property):
+            return None
+
+        # pydantic model fields are like attrs and also can never be hookimpls
+        plugin_is_pydantic_obj = hasattr(plugin, "__pydantic_core_schema__")
+        if plugin_is_pydantic_obj and name in getattr(plugin, "model_fields", {}):
+            # pydantic models mess with the class and attr __signature__
+            # so inspect.isroutine(...) throws exceptions and cant be used
+            return None
+        
+        try:
+            return super().parse_hookimpl_opts(plugin, name)
+        except AttributeError:
+            return super().parse_hookimpl_opts(type(plugin), name)
+
+pm = PatchedPluginManager("abx")
+
+
+
+@hookspec(firstresult=True)
+@hookimpl
+@cache
+def get_PLUGIN_ORDER(plugin: PluginId | Path | ModuleType | Type) -> Tuple[int, Path]:
+    plugin_dir = None
+    plugin_module = None
+    
+    if isinstance(plugin, str) or isinstance(plugin, Path):
+        if str(plugin).endswith('.py'):
+            plugin_dir = Path(plugin).parent
+            plugin_id = plugin_dir.name
+        elif '/' in str(plugin):
+            # assume it's a path to a plugin directory
+            plugin_dir = Path(plugin)
+            plugin_id = plugin_dir.name
+        elif str(plugin).isidentifier():
+            # assume it's a plugin_id
+            plugin_id = str(plugin)
+
+    elif inspect.ismodule(plugin) or inspect.isclass(plugin):
+        plugin_module = plugin
+        plugin_dir = Path(str(plugin_module.__file__)).parent
+        plugin_id = plugin_dir.name
+    else:
+        raise ValueError(f'Invalid plugin, cannot get order: {plugin}')
+
+    if plugin_dir:
+        try:
+            # if .plugin_order file exists, use it to set the load priority
+            order = int((plugin_dir / '.plugin_order').read_text())
+            return (order, plugin_dir)
+        except FileNotFoundError:
+            pass
+    
+    if not plugin_module:
+        try:
+            plugin_module = importlib.import_module(plugin_id)
+        except ImportError:
+            raise ValueError(f'Invalid plugin, cannot get order: {plugin}')
+        
+    if plugin_module and not plugin_dir:
+        plugin_dir = Path(str(plugin_module.__file__)).parent
+    
+    assert plugin_dir
+    
+    return (getattr(plugin_module, '__order__', 999), plugin_dir)
+
+# @hookspec
+# @hookimpl
+# def get_PLUGIN() -> Dict[PluginId, PluginInfo]:
+#     """Get the info for a single plugin, implemented by each plugin"""
+#     return {
+#         __id__: PluginInfo({
+#             'id': __id__,
+#             'package': str(__package__),
+#             'label': __id__,
+#             'version': __version__,
+#             'author': __author__,
+#             'homepage': __homepage__,
+#             'dependencies': __dependencies__,
+#         }),
+#     }
+
+@hookspec(firstresult=True)
+@hookimpl
+@cache
+def get_PLUGIN_METADATA(plugin: PluginId | ModuleType | Type) -> PluginInfo:
+    # TODO: remove get_PLUGIN hook in favor of pyproject.toml and __attr__s metdata
+    # having three methods to detect plugin metadata is overkill
+    
+    assert plugin
+    
+    # import the plugin module by its name
+    if isinstance(plugin, str):
+        module = importlib.import_module(plugin)
+        plugin_id = plugin
+    elif inspect.ismodule(plugin) or inspect.isclass(plugin):
+        module = plugin
+        plugin_id = plugin.__package__
+    else:
+        raise ValueError(f'Invalid plugin, must be a module, class, or plugin ID (package name): {plugin}')
+    
+    assert module.__file__
+    
+    # load the plugin info from the plugin/__init__.py __attr__s if they exist
+    plugin_module_attrs = {
+        'id': getattr(module, '__id__', plugin_id),
+        'name': getattr(module, '__id__', plugin_id),
+        'label': getattr(module, '__label__', plugin_id),
+        'version': getattr(module, '__version__', '0.0.1'),
+        'author': getattr(module, '__author__', 'Unknown'),
+        'homepage': getattr(module, '__homepage__', 'https://github.com/ArchiveBox'),
+        'dependencies': getattr(module, '__dependencies__', []),
+    }
+    
+    # load the plugin info from the plugin.get_PLUGIN() hook method if it has one
+    plugin_info_dict = {}
+    if hasattr(module, 'get_PLUGIN'):
+        plugin_info_dict = {
+            key.lower(): value
+            for key, value in module.get_PLUGIN().items()
+        }
+
+    # load the plugin info from the plugin/pyproject.toml file if it has one
+    plugin_toml_info = {}
+    try:
+        # try loading ./pyproject.toml first in case the plugin is a bare python file not inside a package dir
+        plugin_toml_info = benedict.from_toml((Path(module.__file__).parent / 'pyproject.toml').read_text()).project
+    except Exception:
+        try:
+            # try loading ../pyproject.toml next in case the plugin is in a packge dir
+            plugin_toml_info = benedict.from_toml((Path(module.__file__).parent.parent / 'pyproject.toml').read_text()).project
+        except Exception as e:
+            print('WARNING: could not detect pyproject.toml for PLUGIN:', plugin_id, Path(module.__file__).parent, 'ERROR:', e)
+    
+    # merge the plugin info from all sources + add dyanmically calculated info
+    return cast(PluginInfo, benedict(PluginInfo(**{
+        'id': plugin_id,
+        **plugin_module_attrs,
+        **plugin_info_dict,
+        **plugin_toml_info,
+        'package': module.__package__,
+        'module': module,
+        'order': pm.hook.get_PLUGIN_ORDER(plugin=module),
+        'source_code': module.__file__,
+        'hooks': get_plugin_hooks(module),
+    })))
+    
+@hookspec(firstresult=True)
+@hookimpl
+def get_ALL_PLUGINS() -> Dict[PluginId, PluginInfo]:
+    """Get a flat dictionary of all plugins {plugin_id: {...plugin_metadata}}"""
+    return as_dict(pm.hook.get_PLUGIN())
+
+    
+@hookspec(firstresult=True)
+@hookimpl
+def get_ALL_PLUGINS_METADATA() -> Dict[PluginId, PluginInfo]:
+    """Get the metadata for all the plugins registered with Pluggy."""
+    plugins = {}
+    for plugin_module in pm.get_plugins():
+        plugin_info = pm.hook.get_PLUGIN_METADATA(plugin=plugin_module)
+        assert 'id' in plugin_info
+        plugins[plugin_info['id']] = plugin_info
+    return benedict(plugins)
+
+@hookspec(firstresult=True)
+@hookimpl
+def get_ALL_PLUGIN_HOOK_NAMES() -> Set[str]:
+    """Get a set of all hook names across all plugins"""
+    return {
+        hook_name
+        for plugin_module in pm.get_plugins()
+            for hook_name in get_plugin_hooks(plugin_module)
+    }
+
+pm.add_hookspecs(sys.modules[__name__])
+pm.register(sys.modules[__name__])
+
+
+###### PLUGIN DISCOVERY AND LOADING ########################################################
+
+
+
+def register_hookspecs(plugin_ids: Iterable[PluginId]):
+    """
+    Register all the hookspecs from a list of module names.
+    """
+    for plugin_id in plugin_ids:
+        hookspec_module = importlib.import_module(plugin_id)
+        pm.add_hookspecs(hookspec_module)
+
+
+def find_plugins_in_dir(plugins_dir: Path) -> Dict[PluginId, Path]:
+    """
+    Find all the plugins in a given directory. Just looks for an __init__.py file.
+    """
+    return {
+        plugin_entrypoint.parent.name: plugin_entrypoint.parent
+        for plugin_entrypoint in sorted(plugins_dir.glob("*/__init__.py"), key=pm.hook.get_PLUGIN_ORDER)   # type:ignore
+        if plugin_entrypoint.parent.name != 'abx'
+    }   # "plugins_pkg.pip": "/app/archivebox/plugins_pkg/pip"
+
+
+def get_pip_installed_plugins(group: PluginId='abx') -> Dict[PluginId, Path]:
+    """replaces pm.load_setuptools_entrypoints("abx"), finds plugins that registered entrypoints via pip"""
+    import importlib.metadata
+
+    DETECTED_PLUGINS = {}   # module_name: module_dir_path
+    for dist in list(importlib.metadata.distributions()):
+        for entrypoint in dist.entry_points:
+            if entrypoint.group != group or pm.is_blocked(entrypoint.name):
+                continue
+            DETECTED_PLUGINS[entrypoint.name] = Path(entrypoint.load().__file__).parent
+            # pm.register(plugin, name=ep.name)
+            # pm._plugin_distinfo.append((plugin, DistFacade(dist)))
+    return DETECTED_PLUGINS
+
+
+
+# Load all plugins from pip packages, archivebox built-ins, and user plugins
+def load_plugins(plugins: Iterable[PluginId | ModuleType | Type] | Dict[PluginId, Path]):
+    """
+    Load all the plugins from a dictionary of module names and directory paths.
+    """
+    LOADED_PLUGINS = {}
+    for plugin in plugins:
+        plugin_info = pm.hook.get_PLUGIN_METADATA(plugin=plugin)
+        assert 'id' in plugin_info and 'module' in plugin_info
+        if plugin_info['module'] in pm.get_plugins():
+            LOADED_PLUGINS[plugin_info['id']] = plugin_info
+            continue
+        try:
+            pm.add_hookspecs(plugin_info['module'])
+        except ValueError:
+            # not all plugins register new hookspecs, some only have hookimpls
+            pass
+        pm.register(plugin_info['module'])
+        LOADED_PLUGINS[plugin_info['id']] = plugin_info
+        # print(f'    √ Loaded plugin: {plugin_id}')
+    return benedict(LOADED_PLUGINS)
+
+@cache
+def get_plugin_hooks(plugin: PluginId | ModuleType | Type | None) -> Dict[AttrName, Callable]:
+    """Get all the functions marked with @hookimpl on a module."""
+    if not plugin:
+        return {}
+    
+    hooks = {}
+    
+    if isinstance(plugin, str):
+        plugin_module = importlib.import_module(plugin)
+    elif inspect.ismodule(plugin) or inspect.isclass(plugin):
+        plugin_module = plugin
+    else:
+        raise ValueError(f'Invalid plugin, cannot get hooks: {plugin}')
+    
+    for attr_name in dir(plugin_module):
+        if attr_name.startswith('_'):
+            continue
+        try:
+            attr = getattr(plugin_module, attr_name)
+            if isinstance(attr, Callable):
+                if pm.parse_hookimpl_opts(plugin_module, attr_name):
+                    hooks[attr_name] = attr
+        except Exception as e:
+            print(f'Error getting hookimpls for {plugin}: {e}')
+
+    return hooks
+
+
+def as_list(results) -> List[Any]:
+    """Flatten a list of lists returned by a pm.hook.call() into a single list"""
+    return list(itertools.chain(*results))
+
+
+def as_dict(results: Dict[str, Dict[PluginId, Any]] | List[Dict[PluginId, Any]]) -> Dict[PluginId, Any]:
+    """Flatten a list of dicts returned by a pm.hook.call() into a single dict"""
+    if isinstance(results, (dict, benedict)):
+        results_list = results.values()
+    else:
+        results_list = results
+        
+    return benedict({
+        result_id: result
+        for plugin_results in results_list
+            for result_id, result in dict(plugin_results).items()
+    })
+
+
diff --git a/packages/abx/pyproject.toml b/packages/abx/pyproject.toml
new file mode 100644
index 00000000..3c185653
--- /dev/null
+++ b/packages/abx/pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "abx"
+version = "0.1.0"
+description = "The common shared interfaces for the ABX ArchiveBox plugin ecosystem."
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "pluggy>=1.5.0",
+    "django>=5.1.1,<6.0",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
diff --git a/packages/archivebox-pocket/.circleci/config.yml b/packages/archivebox-pocket/.circleci/config.yml
new file mode 100644
index 00000000..a20a6aae
--- /dev/null
+++ b/packages/archivebox-pocket/.circleci/config.yml
@@ -0,0 +1,61 @@
+version: 2.1
+orbs:
+  python: circleci/python@2.0.3
+
+jobs:
+  build_and_test_3_7:
+    docker:
+      - image: circleci/python:3.7
+    executor: python/default
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+      - run:
+          name: Run tests
+          command: nosetests
+  
+  build_and_test_3_8:
+    docker:
+      - image: circleci/python:3.8
+    executor: python/default
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+      - run:
+          name: Run tests
+          command: nosetests
+          
+  build_and_test_3_9:
+    docker:
+      - image: circleci/python:3.9
+    executor: python/default
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+      - run:
+          name: Run tests
+          command: nosetests
+          
+  build_and_test_3_10:
+    docker:
+      - image: circleci/python:3.10
+    executor: python/default
+    steps:
+      - checkout
+      - python/install-packages:
+          pkg-manager: pip
+      - run:
+          name: Run tests
+          command: nosetests
+
+
+workflows:
+  test_pocket:
+    jobs:
+      - build_and_test_3_7
+      - build_and_test_3_8
+      - build_and_test_3_9
+      - build_and_test_3_10
diff --git a/packages/archivebox-pocket/.gitignore b/packages/archivebox-pocket/.gitignore
new file mode 100644
index 00000000..8acafa3c
--- /dev/null
+++ b/packages/archivebox-pocket/.gitignore
@@ -0,0 +1,43 @@
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+.pypirc
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+
+# Virtualenv
+include/
+lib/
+local/
+.Python
+
+# ViM files
+.*.swp
+.*.swo
+
+# Misc
+*.log
+*.pid
+*.sql
diff --git a/packages/archivebox-pocket/LICENSE.md b/packages/archivebox-pocket/LICENSE.md
new file mode 100644
index 00000000..3b145165
--- /dev/null
+++ b/packages/archivebox-pocket/LICENSE.md
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Tapan Pandita
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice, this
+  list of conditions and the following disclaimer in the documentation and/or
+  other materials provided with the distribution.
+
+* Neither the name of pocket nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/archivebox-pocket/MANIFEST.in b/packages/archivebox-pocket/MANIFEST.in
new file mode 100644
index 00000000..7425f8e8
--- /dev/null
+++ b/packages/archivebox-pocket/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE.md
+include README.md
diff --git a/packages/archivebox-pocket/README.md b/packages/archivebox-pocket/README.md
new file mode 100644
index 00000000..6b2430be
--- /dev/null
+++ b/packages/archivebox-pocket/README.md
@@ -0,0 +1,66 @@
+Pocket
+======
+[![CircleCI](https://img.shields.io/circleci/build/github/tapanpandita/pocket/master?logo=CircleCI)](https://circleci.com/gh/tapanpandita/pocket)
+[![Pypi](https://img.shields.io/pypi/v/pocket.svg)](https://pypi.python.org/pypi/pocket)
+[![PyPI - Downloads](https://img.shields.io/pypi/dm/pocket.svg)](https://pypi.python.org/pypi/pocket)
+![GitHub](https://img.shields.io/github/license/tapanpandita/pocket.svg)
+
+
+A python wrapper for the [pocket api](http://getpocket.com/api/docs).
+
+Installation
+------------
+```
+pip install pocket
+```
+
+Usage
+------
+
+You'll need your pocket consumer key. You can find this from your account page.
+You will also need the access token for the account you want to modify.
+Then, you need to create an instance of the pocket object
+
+```python
+import pocket
+
+pocket_instance = pocket.Pocket(consumer_key, access_token)
+```
+
+### Chaining Modify Methods
+
+All the modify methods can be chained together for creating one bulk query. If you don't wish to chain the methods, just pass `wait=False`.
+
+```python
+import pocket
+
+pocket_instance = pocket.Pocket(consumer_key, access_token)
+
+# perfoms all these actions in one request
+# NOTE: Each individual method returns the instance itself. The response
+# dictionary is not returned till commit is called on the instance.
+response, headers = pocket_instance.archive(item_id1).archive(item_id2).favorite(item_id3).delete(item_id4).commit()
+
+# performs action immediately and returns a dictionary
+pocket_instance.archive(item_id1, wait=False)
+```
+
+### OAUTH
+
+To get request token, use the get_request_token class method. To get the access token use the get_access_token method.
+
+```python
+from pocket import Pocket
+
+request_token = Pocket.get_request_token(consumer_key=consumer_key, redirect_uri=redirect_uri)
+
+# URL to redirect user to, to authorize your app
+auth_url = Pocket.get_auth_url(code=request_token, redirect_uri=redirect_uri)
+# e.g. import subprocess; subprocess.run(['xdg-open', auth_url])
+
+user_credentials = Pocket.get_credentials(consumer_key=consumer_key, code=request_token)
+
+access_token = user_credentials['access_token']
+```
+
+For detailed documentation of the methods available, please visit the official [pocket api documentation](http://getpocket.com/api/docs).
diff --git a/packages/archivebox-pocket/pocket.py b/packages/archivebox-pocket/pocket.py
new file mode 100644
index 00000000..b5b8d2fa
--- /dev/null
+++ b/packages/archivebox-pocket/pocket.py
@@ -0,0 +1,366 @@
+import requests
+import json
+from functools import wraps
+
+
+class PocketException(Exception):
+    '''
+    Base class for all pocket exceptions
+    http://getpocket.com/developer/docs/errors
+
+    '''
+    pass
+
+
+class InvalidQueryException(PocketException):
+    pass
+
+
+class AuthException(PocketException):
+    pass
+
+
+class RateLimitException(PocketException):
+    '''
+    http://getpocket.com/developer/docs/rate-limits
+
+    '''
+    pass
+
+
+class ServerMaintenanceException(PocketException):
+    pass
+
+EXCEPTIONS = {
+    400: InvalidQueryException,
+    401: AuthException,
+    403: RateLimitException,
+    503: ServerMaintenanceException,
+}
+
+
+def method_wrapper(fn):
+
+    @wraps(fn)
+    def wrapped(self, *args, **kwargs):
+        arg_names = list(fn.__code__.co_varnames)
+        arg_names.remove('self')
+        kwargs.update(dict(zip(arg_names, args)))
+
+        url = self.api_endpoints[fn.__name__]
+        payload = dict([
+            (k, v) for k, v in kwargs.items()
+            if v is not None
+        ])
+        payload.update(self.get_payload())
+
+        return self.make_request(url, payload)
+
+    return wrapped
+
+
+def bulk_wrapper(fn):
+
+    @wraps(fn)
+    def wrapped(self, *args, **kwargs):
+        arg_names = list(fn.__code__.co_varnames)
+        arg_names.remove('self')
+        kwargs.update(dict(zip(arg_names, args)))
+
+        wait = kwargs.get('wait', True)
+        query = dict(
+            [(k, v) for k, v in kwargs.items() if v is not None]
+        )
+        # TODO: Fix this hack
+        query['action'] = 'add' if fn.__name__ == 'bulk_add' else fn.__name__
+
+        if wait:
+            self.add_bulk_query(query)
+            return self
+        else:
+            url = self.api_endpoints['send']
+            payload = {
+                'actions': [query],
+            }
+            payload.update(self.get_payload())
+            return self.make_request(
+                url,
+                json.dumps(payload),
+                headers={'content-type': 'application/json'},
+            )
+
+    return wrapped
+
+
+class Pocket(object):
+    '''
+    This class implements a basic python wrapper around the pocket api. For a
+    detailed documentation of the methods and what they do please refer the
+    official pocket api documentation at
+    http://getpocket.com/developer/docs/overview
+
+    '''
+    api_endpoints = dict(
+        (method, 'https://getpocket.com/v3/%s' % method)
+        for method in "add,send,get".split(",")
+    )
+
+    statuses = {
+        200: 'Request was successful',
+        400: 'Invalid request, please make sure you follow the '
+             'documentation for proper syntax',
+        401: 'Problem authenticating the user',
+        403: 'User was authenticated, but access denied due to lack of '
+             'permission or rate limiting',
+        503: 'Pocket\'s sync server is down for scheduled maintenance.',
+    }
+
+    def __init__(self, consumer_key, access_token):
+        self.consumer_key = consumer_key
+        self.access_token = access_token
+        self._bulk_query = []
+
+        self._payload = {
+            'consumer_key': self.consumer_key,
+            'access_token': self.access_token,
+        }
+
+    def get_payload(self):
+        return self._payload
+
+    def add_bulk_query(self, query):
+        self._bulk_query.append(query)
+
+    @staticmethod
+    def _post_request(url, payload, headers):
+        r = requests.post(url, data=payload, headers=headers)
+        return r
+
+    @classmethod
+    def _make_request(cls, url, payload, headers=None):
+        r = cls._post_request(url, payload, headers)
+
+        if r.status_code > 399:
+            error_msg = cls.statuses.get(r.status_code)
+            extra_info = r.headers.get('X-Error')
+            raise EXCEPTIONS.get(r.status_code, PocketException)(
+                '%s. %s' % (error_msg, extra_info)
+            )
+
+        return r.json() or r.text, r.headers
+
+    @classmethod
+    def make_request(cls, url, payload, headers=None):
+        return cls._make_request(url, payload, headers)
+
+    @method_wrapper
+    def add(self, url, title=None, tags=None, tweet_id=None):
+        '''
+        This method allows you to add a page to a user's list.
+        In order to use the /v3/add endpoint, your consumer key must have the
+        "Add" permission.
+        http://getpocket.com/developer/docs/v3/add
+
+        '''
+
+    @method_wrapper
+    def get(
+        self, state=None, favorite=None, tag=None, contentType=None,
+        sort=None, detailType=None, search=None, domain=None, since=None,
+        count=None, offset=None
+    ):
+        '''
+        This method allows you to retrieve a user's list. It supports
+        retrieving items changed since a specific time to allow for syncing.
+        http://getpocket.com/developer/docs/v3/retrieve
+
+        '''
+
+    @method_wrapper
+    def send(self, actions):
+        '''
+        This method allows you to make changes to a user's list. It supports
+        adding new pages, marking pages as read, changing titles, or updating
+        tags. Multiple changes to items can be made in one request.
+        http://getpocket.com/developer/docs/v3/modify
+
+        '''
+
+    @bulk_wrapper
+    def bulk_add(
+        self, item_id, ref_id=None, tags=None, time=None, title=None,
+        url=None, wait=True
+    ):
+        '''
+        Add a new item to the user's list
+        http://getpocket.com/developer/docs/v3/modify#action_add
+
+        '''
+
+    @bulk_wrapper
+    def archive(self, item_id, time=None, wait=True):
+        '''
+        Move an item to the user's archive
+        http://getpocket.com/developer/docs/v3/modify#action_archive
+
+        '''
+
+    @bulk_wrapper
+    def readd(self, item_id, time=None, wait=True):
+        '''
+        Re-add (unarchive) an item to the user's list
+        http://getpocket.com/developer/docs/v3/modify#action_readd
+
+        '''
+
+    @bulk_wrapper
+    def favorite(self, item_id, time=None, wait=True):
+        '''
+        Mark an item as a favorite
+        http://getpocket.com/developer/docs/v3/modify#action_favorite
+
+        '''
+
+    @bulk_wrapper
+    def unfavorite(self, item_id, time=None, wait=True):
+        '''
+        Remove an item from the user's favorites
+        http://getpocket.com/developer/docs/v3/modify#action_unfavorite
+
+        '''
+
+    @bulk_wrapper
+    def delete(self, item_id, time=None, wait=True):
+        '''
+        Permanently remove an item from the user's account
+        http://getpocket.com/developer/docs/v3/modify#action_delete
+
+        '''
+
+    @bulk_wrapper
+    def tags_add(self, item_id, tags, time=None, wait=True):
+        '''
+        Add one or more tags to an item
+        http://getpocket.com/developer/docs/v3/modify#action_tags_add
+
+        '''
+
+    @bulk_wrapper
+    def tags_remove(self, item_id, tags, time=None, wait=True):
+        '''
+        Remove one or more tags from an item
+        http://getpocket.com/developer/docs/v3/modify#action_tags_remove
+
+        '''
+
+    @bulk_wrapper
+    def tags_replace(self, item_id, tags, time=None, wait=True):
+        '''
+        Replace all of the tags for an item with one or more provided tags
+        http://getpocket.com/developer/docs/v3/modify#action_tags_replace
+
+        '''
+
+    @bulk_wrapper
+    def tags_clear(self, item_id, time=None, wait=True):
+        '''
+        Remove all tags from an item.
+        http://getpocket.com/developer/docs/v3/modify#action_tags_clear
+
+        '''
+
+    @bulk_wrapper
+    def tag_rename(self, item_id, old_tag, new_tag, time=None, wait=True):
+        '''
+        Rename a tag. This affects all items with this tag.
+        http://getpocket.com/developer/docs/v3/modify#action_tag_rename
+
+        '''
+
+    def commit(self):
+        '''
+        This method executes the bulk query, flushes stored queries and
+        returns the response
+
+        '''
+        url = self.api_endpoints['send']
+        payload = {
+            'actions': self._bulk_query,
+        }
+        payload.update(self._payload)
+        self._bulk_query = []
+
+        return self._make_request(
+            url,
+            json.dumps(payload),
+            headers={'content-type': 'application/json'},
+        )
+
+    @classmethod
+    def get_request_token(
+        cls, consumer_key, redirect_uri='http://example.com/', state=None
+    ):
+        '''
+        Returns the request token that can be used to fetch the access token
+
+        '''
+        headers = {
+            'X-Accept': 'application/json',
+        }
+        url = 'https://getpocket.com/v3/oauth/request'
+        payload = {
+            'consumer_key': consumer_key,
+            'redirect_uri': redirect_uri,
+        }
+
+        if state:
+            payload['state'] = state
+
+        return cls._make_request(url, payload, headers)[0]['code']
+
+    @classmethod
+    def get_credentials(cls, consumer_key, code):
+        '''
+        Fetches access token from using the request token and consumer key
+
+        '''
+        headers = {
+            'X-Accept': 'application/json',
+        }
+        url = 'https://getpocket.com/v3/oauth/authorize'
+        payload = {
+            'consumer_key': consumer_key,
+            'code': code,
+        }
+
+        return cls._make_request(url, payload, headers)[0]
+
+    @classmethod
+    def get_access_token(cls, consumer_key, code):
+        return cls.get_credentials(consumer_key, code)['access_token']
+
+    @classmethod
+    def get_auth_url(cls, code, redirect_uri='http://example.com'):
+        auth_url = ('https://getpocket.com/auth/authorize'
+                    '?request_token=%s&redirect_uri=%s' % (code, redirect_uri))
+        return auth_url
+
+    @classmethod
+    def auth(
+        cls, consumer_key, redirect_uri='http://example.com/', state=None,
+    ):
+        '''
+        This is a test method for verifying if oauth worked
+        http://getpocket.com/developer/docs/authentication
+
+        '''
+        code = cls.get_request_token(consumer_key, redirect_uri, state)
+
+        auth_url = 'https://getpocket.com/auth/authorize?request_token='\
+            '%s&redirect_uri=%s' % (code, redirect_uri)
+        raw_input(
+            'Please open %s in your browser to authorize the app and '
+            'press enter:' % auth_url
+        )
+
+        return cls.get_access_token(consumer_key, code)
diff --git a/packages/archivebox-pocket/pyproject.toml b/packages/archivebox-pocket/pyproject.toml
new file mode 100644
index 00000000..6acf8a57
--- /dev/null
+++ b/packages/archivebox-pocket/pyproject.toml
@@ -0,0 +1,19 @@
+[project]
+name = "archivebox-pocket"
+version = "0.3.7"
+description = " api wrapper for getpocket.com"
+readme = "README.md"
+requires-python = ">=3.10"
+dependencies = [
+    "requests>=2.32.3",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.sdist]
+packages = ["."]
+
+[tool.hatch.build.targets.wheel]
+packages = ["."]
diff --git a/packages/archivebox-pocket/requirements.txt b/packages/archivebox-pocket/requirements.txt
new file mode 100644
index 00000000..9598beea
--- /dev/null
+++ b/packages/archivebox-pocket/requirements.txt
@@ -0,0 +1,4 @@
+coverage==3.7.1
+mock==1.0.1
+nose==1.3.0
+requests==2.20.0
diff --git a/packages/archivebox-pocket/setup.py b/packages/archivebox-pocket/setup.py
new file mode 100644
index 00000000..5a5baba0
--- /dev/null
+++ b/packages/archivebox-pocket/setup.py
@@ -0,0 +1,41 @@
+from setuptools import setup
+
+setup(
+    name = "pocket", # pip install pocket
+    description = "api wrapper for getpocket.com",
+    #long_description=open('README.md', 'rt').read(),
+
+    # version
+    # third part for minor release
+    # second when api changes
+    # first when it becomes stable someday
+    version = "0.3.7",
+    author = 'Tapan Pandita',
+    author_email = "tapan.pandita@gmail.com",
+
+    url = 'http://github.com/tapanpandita/pocket/',
+    license = 'BSD',
+
+    # as a practice no need to hard code version unless you know program wont
+    # work unless the specific versions are used
+    install_requires = ["requests>=2.32.3"],
+
+    py_modules = ["pocket"],
+
+    zip_safe = True,
+)
+
+# TODO: Do all this and delete these lines
+# register: Create an accnt on pypi, store your credentials in ~/.pypirc:
+#
+# [pypirc]
+# servers =
+#     pypi
+#
+# [server-login]
+# username:<username>
+# password:<pass>
+#
+# $ python setup.py register # one time only, will create pypi page for pocket
+# $ python setup.py sdist --formats=gztar,zip upload # create a new release
+#
diff --git a/packages/archivebox-pocket/test_pocket.py b/packages/archivebox-pocket/test_pocket.py
new file mode 100644
index 00000000..14e67f53
--- /dev/null
+++ b/packages/archivebox-pocket/test_pocket.py
@@ -0,0 +1,52 @@
+import unittest
+import pocket
+from mock import patch
+
+
+class PocketTest(unittest.TestCase):
+
+    def setUp(self):
+        self.consumer_key = 'consumer_key'
+        self.access_token = 'access_token'
+
+    def tearDown(self):
+        pass
+
+    def test_pocket_init(self):
+        pocket_instance = pocket.Pocket(
+            self.consumer_key,
+            self.access_token,
+        )
+
+        self.assertEqual(self.consumer_key, pocket_instance.consumer_key)
+        self.assertEqual(self.access_token, pocket_instance.access_token)
+
+    def test_pocket_init_payload(self):
+        pocket_instance = pocket.Pocket(
+            self.consumer_key,
+            self.access_token,
+        )
+        expected_payload = {
+            'consumer_key': self.consumer_key,
+            'access_token': self.access_token,
+        }
+
+        self.assertEqual(expected_payload, pocket_instance._payload)
+
+    def test_post_request(self):
+        mock_payload = {
+            'consumer_key': self.consumer_key,
+            'access_token': self.access_token,
+        }
+        mock_url = 'https://getpocket.com/v3/'
+        mock_headers = {
+            'content-type': 'application/json',
+        }
+
+        with patch('pocket.requests') as mock_requests:
+            pocket.Pocket._post_request(mock_url, mock_payload, mock_headers)
+            mock_requests.post.assert_called_once_with(
+                mock_url,
+                data=mock_payload,
+                headers=mock_headers,
+            )
diff --git a/packages/pydantic-pkgr b/packages/pydantic-pkgr
new file mode 160000
index 00000000..a116eaef
--- /dev/null
+++ b/packages/pydantic-pkgr
@@ -0,0 +1 @@
+Subproject commit a116eaef7f090dc872b18e82b5a538313075ded6
diff --git a/pyproject.toml b/pyproject.toml
index c75f0641..de870ada 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "archivebox"
-version = "0.8.5rc51"
+version = "0.8.5rc53"
 requires-python = ">=3.10"
 description = "Self-hosted internet archiving solution."
 authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
@@ -46,6 +46,7 @@ dependencies = [
     "django-ninja>=1.3.0",
     "django-extensions>=3.2.3",
     "mypy-extensions>=1.0.0",
+    "typing_extensions>=4.12.2",
     "channels[daphne]>=4.1.0",
     "django-signal-webhooks>=0.3.0",
     "django-admin-data-views>=0.4.1",
@@ -80,6 +81,22 @@ dependencies = [
     # "pocket@git+https://github.com/tapanpandita/pocket.git@v0.3.7",
     "pydantic-pkgr>=0.5.4",
     ############# Plugin Dependencies ################
+    "abx>=0.1.0",
+    "abx-spec-pydantic-pkgr>=0.1.0",
+    "abx-spec-config>=0.1.0",
+    "abx-spec-archivebox>=0.1.0",
+    "abx-spec-django>=0.1.0",
+    "abx-spec-extractor>=0.1.0",
+    "abx-spec-searchbackend>=0.1.0",
+
+    "abx-plugin-default-binproviders>=2024.10.24",
+    "abx-plugin-pip-binprovider>=2024.10.24",
+    "abx-plugin-npm-binprovider>=2024.10.24",
+    "abx-plugin-playwright-binprovider>=2024.10.24",
+
+    # "abx-plugin-pocket",
+    # "abx-plugin-sonic",
+    # "abx-plugin-yt-dlp",
     "sonic-client>=1.0.0",
     "yt-dlp>=2024.8.6",               # for: media"
 ]
@@ -104,14 +121,14 @@ all = [
 [tool.uv]
 dev-dependencies = [
     ### BUILD
-    "uv",
+    "uv>=0.4.26",
     "pip>=24.2",
     "setuptools>=75.1.0",
     "wheel>=0.44.0",
     "homebrew-pypi-poet>=0.10.0",      # for: generating archivebox.rb brewfile list of python packages
     ### DOCS
     "recommonmark>=0.7.1",
-    "sphinx",
+    "sphinx>=8.1.3",
     "sphinx-rtd-theme>=2.0.0",
     ### DEBUGGING
     "django-debug-toolbar>=4.4.6",
@@ -121,7 +138,7 @@ dev-dependencies = [
     "logfire[django]>=0.51.0",
     "opentelemetry-instrumentation-django>=0.47b0",
     "opentelemetry-instrumentation-sqlite3>=0.47b0",
-    "viztracer",                                     # usage: viztracer ../.venv/bin/archivebox manage check
+    "viztracer>=0.17.0",                               # usage: viztracer ../.venv/bin/archivebox manage check
     # "snakeviz",                                      # usage: python -m cProfile -o flamegraph.prof ../.venv/bin/archivebox manage check
     ### TESTING
     "pytest>=8.3.3",
@@ -133,6 +150,26 @@ dev-dependencies = [
     "django-autotyping>=0.5.1",
 ]
 
+[tool.uv.sources]
+abx = { workspace = true }
+abx-spec-pydantic-pkgr = { workspace = true }
+abx-spec-config = { workspace = true }
+abx-spec-archivebox = { workspace = true }
+abx-spec-django = { workspace = true }
+abx-spec-extractor = { workspace = true }
+abx-spec-searchbackend = { workspace = true }
+
+abx-plugin-default-binproviders = { workspace = true }
+abx-plugin-pip-binprovider = { workspace = true }
+abx-plugin-npm-binprovider = { workspace = true }
+abx-plugin-playwright-binprovider = { workspace = true }
+
+pydantic-pkgr = { workspace = true }
+archivebox-pocket = { workspace = true }
+
+[tool.uv.workspace]
+members = ["packages/*"]
+
 [build-system]
 requires = ["pdm-backend"]
 build-backend = "pdm.backend"
diff --git a/requirements.txt b/requirements.txt
index f9a37b4b..db2a66f7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -166,7 +166,7 @@ parso==0.8.4
     # via jedi
 pexpect==4.9.0
     # via ipython
-phonenumbers==8.13.47
+phonenumbers==8.13.48
     # via python-benedict
 platformdirs==4.3.6
     # via pydantic-pkgr
@@ -250,7 +250,7 @@ requests==2.32.3
     #   archivebox (pyproject.toml)
     #   python-benedict
     #   yt-dlp
-rich==13.9.2
+rich==13.9.3
     # via
     #   archivebox (pyproject.toml)
     #   rich-argparse
@@ -332,7 +332,7 @@ xlrd==2.0.1
     # via python-benedict
 xmltodict==0.14.2
     # via python-benedict
-yt-dlp==2024.10.7
+yt-dlp==2024.10.22
     # via archivebox (pyproject.toml)
-zope-interface==7.1.0
+zope-interface==7.1.1
     # via twisted
diff --git a/uv.lock b/uv.lock
index f320d661..e4d6e7e4 100644
--- a/uv.lock
+++ b/uv.lock
@@ -6,6 +6,329 @@ resolution-markers = [
     "python_full_version >= '3.13'",
 ]
 
+[manifest]
+members = [
+    "abx",
+    "abx-archivedotorg-extractor",
+    "abx-chrome-extractor",
+    "abx-curl-extractor",
+    "abx-favicon-extractor",
+    "abx-git-extractor",
+    "abx-htmltotext-extractor",
+    "abx-ldap-auth",
+    "abx-mercury-extractor",
+    "abx-plugin-default-binproviders",
+    "abx-plugin-npm-binprovider",
+    "abx-plugin-pip-binprovider",
+    "abx-plugin-playwright-binprovider",
+    "abx-pocket-extractor",
+    "abx-puppeteer-binprovider",
+    "abx-readability-extractor",
+    "abx-readwise-extractor",
+    "abx-ripgrep-search",
+    "abx-singlefile-extractor",
+    "abx-sonic-search",
+    "abx-spec-archivebox",
+    "abx-spec-config",
+    "abx-spec-django",
+    "abx-spec-extractor",
+    "abx-spec-pydantic-pkgr",
+    "abx-spec-searchbackend",
+    "abx-sqlitefts-search",
+    "abx-wget-extractor",
+    "abx-ytdlp-extractor",
+    "archivebox",
+    "archivebox-pocket",
+    "pydantic-pkgr",
+]
+
+[[package]]
+name = "abx"
+version = "0.1.0"
+source = { editable = "packages/abx" }
+dependencies = [
+    { name = "django" },
+    { name = "pluggy" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "django", specifier = ">=5.1.1,<6.0" },
+    { name = "pluggy", specifier = ">=1.5.0" },
+]
+
+[[package]]
+name = "abx-archivedotorg-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-archivedotorg-extractor" }
+
+[[package]]
+name = "abx-chrome-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-chrome-extractor" }
+
+[[package]]
+name = "abx-curl-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-curl-extractor" }
+
+[[package]]
+name = "abx-favicon-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-favicon-extractor" }
+
+[[package]]
+name = "abx-git-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-git-extractor" }
+
+[[package]]
+name = "abx-htmltotext-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-htmltotext-extractor" }
+
+[[package]]
+name = "abx-ldap-auth"
+version = "0.1.0"
+source = { editable = "packages/abx-plugin-ldap-auth" }
+
+[[package]]
+name = "abx-mercury-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-mercury-extractor" }
+
+[[package]]
+name = "abx-plugin-default-binproviders"
+version = "2024.10.24"
+source = { editable = "packages/abx-plugin-default-binproviders" }
+dependencies = [
+    { name = "abx" },
+    { name = "abx-spec-pydantic-pkgr" },
+    { name = "pydantic-pkgr" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "abx-spec-pydantic-pkgr", editable = "packages/abx-spec-pydantic-pkgr" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
+]
+
+[[package]]
+name = "abx-plugin-npm-binprovider"
+version = "2024.10.24"
+source = { editable = "packages/abx-plugin-npm-binprovider" }
+dependencies = [
+    { name = "abx" },
+    { name = "abx-plugin-default-binproviders" },
+    { name = "abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr" },
+    { name = "pydantic-pkgr" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "abx-plugin-default-binproviders", editable = "packages/abx-plugin-default-binproviders" },
+    { name = "abx-spec-config", editable = "packages/abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr", editable = "packages/abx-spec-pydantic-pkgr" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
+]
+
+[[package]]
+name = "abx-plugin-pip-binprovider"
+version = "2024.10.24"
+source = { editable = "packages/abx-plugin-pip-binprovider" }
+dependencies = [
+    { name = "abx" },
+    { name = "abx-plugin-default-binproviders" },
+    { name = "abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr" },
+    { name = "django" },
+    { name = "pydantic-pkgr" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "abx-plugin-default-binproviders", editable = "packages/abx-plugin-default-binproviders" },
+    { name = "abx-spec-config", editable = "packages/abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr", editable = "packages/abx-spec-pydantic-pkgr" },
+    { name = "django", specifier = ">=5.0.0" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
+]
+
+[[package]]
+name = "abx-plugin-playwright-binprovider"
+version = "2024.10.24"
+source = { editable = "packages/abx-plugin-playwright-binprovider" }
+dependencies = [
+    { name = "abx" },
+    { name = "abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr" },
+    { name = "pydantic" },
+    { name = "pydantic-pkgr" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "abx-spec-config", editable = "packages/abx-spec-config" },
+    { name = "abx-spec-pydantic-pkgr", editable = "packages/abx-spec-pydantic-pkgr" },
+    { name = "pydantic", specifier = ">=2.4.2" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
+]
+
+[[package]]
+name = "abx-pocket-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-pocket-extractor" }
+
+[[package]]
+name = "abx-puppeteer-binprovider"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-puppeteer-binprovider" }
+
+[[package]]
+name = "abx-readability-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-readability-extractor" }
+
+[[package]]
+name = "abx-readwise-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-readwise-extractor" }
+
+[[package]]
+name = "abx-ripgrep-search"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-ripgrep-search" }
+
+[[package]]
+name = "abx-singlefile-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-singlefile-extractor" }
+
+[[package]]
+name = "abx-sonic-search"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-sonic-search" }
+
+[[package]]
+name = "abx-spec-archivebox"
+version = "0.1.0"
+source = { editable = "packages/abx-spec-archivebox" }
+dependencies = [
+    { name = "abx" },
+    { name = "django" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "django", specifier = ">=5.1.1,<6.0" },
+]
+
+[[package]]
+name = "abx-spec-config"
+version = "0.0.1"
+source = { editable = "packages/abx-spec-config" }
+dependencies = [
+    { name = "abx" },
+    { name = "pydantic" },
+    { name = "pydantic-settings" },
+    { name = "python-benedict" },
+    { name = "rich" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "pydantic", specifier = ">=2.9.2" },
+    { name = "pydantic-settings", specifier = ">=2.6.0" },
+    { name = "python-benedict", specifier = ">=0.34.0" },
+    { name = "rich", specifier = ">=13.9.3" },
+]
+
+[[package]]
+name = "abx-spec-django"
+version = "0.1.0"
+source = { editable = "packages/abx-spec-django" }
+dependencies = [
+    { name = "abx" },
+    { name = "django" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "django", specifier = ">=5.1.1,<6.0" },
+]
+
+[[package]]
+name = "abx-spec-extractor"
+version = "0.1.0"
+source = { editable = "packages/abx-spec-extractor" }
+dependencies = [
+    { name = "abx" },
+    { name = "pydantic" },
+    { name = "python-benedict" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "pydantic", specifier = ">=2.5.0" },
+    { name = "python-benedict", specifier = ">=0.26.0" },
+]
+
+[[package]]
+name = "abx-spec-pydantic-pkgr"
+version = "0.1.0"
+source = { editable = "packages/abx-spec-pydantic-pkgr" }
+dependencies = [
+    { name = "abx" },
+    { name = "pydantic-pkgr" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
+]
+
+[[package]]
+name = "abx-spec-searchbackend"
+version = "0.1.0"
+source = { editable = "packages/abx-spec-searchbackend" }
+dependencies = [
+    { name = "abx" },
+    { name = "pydantic" },
+    { name = "python-benedict" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "pydantic", specifier = ">=2.5.0" },
+    { name = "python-benedict", specifier = ">=0.26.0" },
+]
+
+[[package]]
+name = "abx-sqlitefts-search"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-sqlitefts-search" }
+
+[[package]]
+name = "abx-wget-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-wget-extractor" }
+
+[[package]]
+name = "abx-ytdlp-extractor"
+version = "0.1.0"
+source = { virtual = "packages/abx-plugin-ytdlp-extractor" }
+
 [[package]]
 name = "alabaster"
 version = "1.0.0"
@@ -24,6 +347,49 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
 ]
 
+[[package]]
+name = "ansible"
+version = "10.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "ansible-core" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d7/23/ae30b280ebad1f19fa012c0410aaf7d50cd741a5786bd60a2ecba42d2cd4/ansible-10.5.0.tar.gz", hash = "sha256:ba2045031a7d60c203b6e5fe1f8eaddd53ae076f7ada910e636494384135face", size = 40391062 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2e/33/4cb64286f44cd36753cd15ef636be6c9e40be331e14e97caca74cb7a3242/ansible-10.5.0-py3-none-any.whl", hash = "sha256:1d10bddba58f1edd0fe0b8e0387e0fafc519535066bb3c919c33b6ea3ec32a0f", size = 48977627 },
+]
+
+[[package]]
+name = "ansible-core"
+version = "2.17.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cryptography" },
+    { name = "jinja2" },
+    { name = "packaging" },
+    { name = "pyyaml" },
+    { name = "resolvelib" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/39/96/02a6d1d16ef3b08d53e23db519fbb31641b2767404b674f3ea71c7c1ac3b/ansible_core-2.17.5.tar.gz", hash = "sha256:ae7f51fd13dc9d57c9bcd43ef23f9c255ca8f18f4b5c0011a4f9b724d92c5a8e", size = 3097858 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9e/4f/5c344dc52327766fb286771d492481c2c60eace9697497b250e1d79b1e40/ansible_core-2.17.5-py3-none-any.whl", hash = "sha256:10f165b475cf2bc8d886e532cadb32c52ee6a533649793101d3166bca9bd3ea3", size = 2193938 },
+]
+
+[[package]]
+name = "ansible-runner"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "packaging" },
+    { name = "pexpect" },
+    { name = "python-daemon" },
+    { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e0/b4/842698d5c17b3cae7948df4c812e01f4199dfb9f35b1c0bb51cf2fe5c246/ansible-runner-2.4.0.tar.gz", hash = "sha256:82d02b2548830f37a53517b65c823c4af371069406c7d213b5c9041d45e0c5b6", size = 148802 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/58/46/44577e2e58de8b9c9398e1ee08b6c697bb2581446209cbfd2639cced66f5/ansible_runner-2.4.0-py3-none-any.whl", hash = "sha256:a3f592ae4cdfa62a72ad15de60da9c8210f376d67f495c4a78d4cf1dc7ccdf89", size = 79678 },
+]
+
 [[package]]
 name = "anyio"
 version = "4.6.2.post1"
@@ -41,9 +407,20 @@ wheels = [
 
 [[package]]
 name = "archivebox"
-version = "0.8.5rc50"
+version = "0.8.5rc53"
 source = { editable = "." }
 dependencies = [
+    { name = "abx" },
+    { name = "abx-plugin-default-binproviders" },
+    { name = "abx-plugin-npm-binprovider" },
+    { name = "abx-plugin-pip-binprovider" },
+    { name = "abx-plugin-playwright-binprovider" },
+    { name = "abx-spec-archivebox" },
+    { name = "abx-spec-config" },
+    { name = "abx-spec-django" },
+    { name = "abx-spec-extractor" },
+    { name = "abx-spec-pydantic-pkgr" },
+    { name = "abx-spec-searchbackend" },
     { name = "atomicwrites" },
     { name = "base32-crockford" },
     { name = "channels", extra = ["daphne"] },
@@ -79,6 +456,7 @@ dependencies = [
     { name = "sonic-client" },
     { name = "supervisor" },
     { name = "typeid-python" },
+    { name = "typing-extensions" },
     { name = "ulid-py" },
     { name = "w3lib" },
     { name = "yt-dlp" },
@@ -122,6 +500,17 @@ dev = [
 
 [package.metadata]
 requires-dist = [
+    { name = "abx", editable = "packages/abx" },
+    { name = "abx-plugin-default-binproviders", editable = "packages/abx-plugin-default-binproviders" },
+    { name = "abx-plugin-npm-binprovider", editable = "packages/abx-plugin-npm-binprovider" },
+    { name = "abx-plugin-pip-binprovider", editable = "packages/abx-plugin-pip-binprovider" },
+    { name = "abx-plugin-playwright-binprovider", editable = "packages/abx-plugin-playwright-binprovider" },
+    { name = "abx-spec-archivebox", editable = "packages/abx-spec-archivebox" },
+    { name = "abx-spec-config", editable = "packages/abx-spec-config" },
+    { name = "abx-spec-django", editable = "packages/abx-spec-django" },
+    { name = "abx-spec-extractor", editable = "packages/abx-spec-extractor" },
+    { name = "abx-spec-pydantic-pkgr", editable = "packages/abx-spec-pydantic-pkgr" },
+    { name = "abx-spec-searchbackend", editable = "packages/abx-spec-searchbackend" },
     { name = "archivebox", extras = ["sonic", "ldap"], marker = "extra == 'all'" },
     { name = "atomicwrites", specifier = "==1.4.1" },
     { name = "base32-crockford", specifier = "==0.3.0" },
@@ -148,7 +537,7 @@ requires-dist = [
     { name = "pluggy", specifier = ">=1.5.0" },
     { name = "psutil", specifier = ">=6.0.0" },
     { name = "py-machineid", specifier = ">=0.6.0" },
-    { name = "pydantic-pkgr", specifier = ">=0.5.4" },
+    { name = "pydantic-pkgr", editable = "packages/pydantic-pkgr" },
     { name = "pydantic-settings", specifier = ">=2.5.2" },
     { name = "python-benedict", extras = ["io", "parse"], specifier = ">=0.33.2" },
     { name = "python-crontab", specifier = ">=3.2.0" },
@@ -160,6 +549,7 @@ requires-dist = [
     { name = "sonic-client", specifier = ">=1.0.0" },
     { name = "supervisor", specifier = ">=4.2.5" },
     { name = "typeid-python", specifier = ">=0.3.1" },
+    { name = "typing-extensions", specifier = ">=4.12.2" },
     { name = "ulid-py", specifier = ">=1.1.0" },
     { name = "w3lib", specifier = ">=2.2.1" },
     { name = "yt-dlp", specifier = ">=2024.8.6" },
@@ -184,13 +574,24 @@ dev = [
     { name = "requests-tracker", specifier = ">=0.3.3" },
     { name = "ruff", specifier = ">=0.6.6" },
     { name = "setuptools", specifier = ">=75.1.0" },
-    { name = "sphinx" },
+    { name = "sphinx", specifier = ">=8.1.3" },
     { name = "sphinx-rtd-theme", specifier = ">=2.0.0" },
-    { name = "uv" },
-    { name = "viztracer" },
+    { name = "uv", specifier = ">=0.4.26" },
+    { name = "viztracer", specifier = ">=0.17.0" },
     { name = "wheel", specifier = ">=0.44.0" },
 ]
 
+[[package]]
+name = "archivebox-pocket"
+version = "0.3.7"
+source = { editable = "packages/archivebox-pocket" }
+dependencies = [
+    { name = "requests" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "requests", specifier = ">=2.32.3" }]
+
 [[package]]
 name = "asgiref"
 version = "3.8.1"
@@ -272,6 +673,38 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/4d/6f/7ad1176c56c920e9841b14923f81545a4243876628312f143915561770d2/base32_crockford-0.3.0-py2.py3-none-any.whl", hash = "sha256:295ef5ffbf6ed96b6e739ffd36be98fa7e90a206dd18c39acefb15777eedfe6e", size = 5050 },
 ]
 
+[[package]]
+name = "bcrypt"
+version = "4.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e4/7e/d95e7d96d4828e965891af92e43b52a4cd3395dc1c1ef4ee62748d0471d0/bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221", size = 24294 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a9/81/4e8f5bc0cd947e91fb720e1737371922854da47a94bc9630454e7b2845f8/bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb", size = 471568 },
+    { url = "https://files.pythonhosted.org/packages/05/d2/1be1e16aedec04bcf8d0156e01b987d16a2063d38e64c3f28030a3427d61/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00", size = 277372 },
+    { url = "https://files.pythonhosted.org/packages/e3/96/7a654027638ad9b7589effb6db77eb63eba64319dfeaf9c0f4ca953e5f76/bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d", size = 273488 },
+    { url = "https://files.pythonhosted.org/packages/46/54/dc7b58abeb4a3d95bab653405935e27ba32f21b812d8ff38f271fb6f7f55/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291", size = 277759 },
+    { url = "https://files.pythonhosted.org/packages/ac/be/da233c5f11fce3f8adec05e8e532b299b64833cc962f49331cdd0e614fa9/bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328", size = 273796 },
+    { url = "https://files.pythonhosted.org/packages/b0/b8/8b4add88d55a263cf1c6b8cf66c735280954a04223fcd2880120cc767ac3/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7", size = 311082 },
+    { url = "https://files.pythonhosted.org/packages/7b/76/2aa660679abbdc7f8ee961552e4bb6415a81b303e55e9374533f22770203/bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399", size = 305912 },
+    { url = "https://files.pythonhosted.org/packages/00/03/2af7c45034aba6002d4f2b728c1a385676b4eab7d764410e34fd768009f2/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060", size = 325185 },
+    { url = "https://files.pythonhosted.org/packages/dc/5d/6843443ce4ab3af40bddb6c7c085ed4a8418b3396f7a17e60e6d9888416c/bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7", size = 335188 },
+    { url = "https://files.pythonhosted.org/packages/cb/4c/ff8ca83d816052fba36def1d24e97d9a85739b9bbf428c0d0ecd296a07c8/bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458", size = 156481 },
+    { url = "https://files.pythonhosted.org/packages/65/f1/e09626c88a56cda488810fb29d5035f1662873777ed337880856b9d204ae/bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5", size = 151336 },
+    { url = "https://files.pythonhosted.org/packages/96/86/8c6a84daed4dd878fbab094400c9174c43d9b838ace077a2f8ee8bc3ae12/bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841", size = 472414 },
+    { url = "https://files.pythonhosted.org/packages/f6/05/e394515f4e23c17662e5aeb4d1859b11dc651be01a3bd03c2e919a155901/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68", size = 277599 },
+    { url = "https://files.pythonhosted.org/packages/4b/3b/ad784eac415937c53da48983756105d267b91e56aa53ba8a1b2014b8d930/bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe", size = 273491 },
+    { url = "https://files.pythonhosted.org/packages/cc/14/b9ff8e0218bee95e517b70e91130effb4511e8827ac1ab00b4e30943a3f6/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2", size = 277934 },
+    { url = "https://files.pythonhosted.org/packages/3e/d0/31938bb697600a04864246acde4918c4190a938f891fd11883eaaf41327a/bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c", size = 273804 },
+    { url = "https://files.pythonhosted.org/packages/e7/c3/dae866739989e3f04ae304e1201932571708cb292a28b2f1b93283e2dcd8/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae", size = 311275 },
+    { url = "https://files.pythonhosted.org/packages/5d/2c/019bc2c63c6125ddf0483ee7d914a405860327767d437913942b476e9c9b/bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d", size = 306355 },
+    { url = "https://files.pythonhosted.org/packages/75/fe/9e137727f122bbe29771d56afbf4e0dbc85968caa8957806f86404a5bfe1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e", size = 325381 },
+    { url = "https://files.pythonhosted.org/packages/1a/d4/586b9c18a327561ea4cd336ff4586cca1a7aa0f5ee04e23a8a8bb9ca64f1/bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8", size = 335685 },
+    { url = "https://files.pythonhosted.org/packages/24/55/1a7127faf4576138bb278b91e9c75307490178979d69c8e6e273f74b974f/bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34", size = 155857 },
+    { url = "https://files.pythonhosted.org/packages/1c/2a/c74052e54162ec639266d91539cca7cbf3d1d3b8b36afbfeaee0ea6a1702/bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9", size = 151717 },
+    { url = "https://files.pythonhosted.org/packages/09/97/01026e7b1b7f8aeb41514408eca1137c0f8aef9938335e3bc713f82c282e/bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a", size = 275924 },
+    { url = "https://files.pythonhosted.org/packages/ca/46/03eb26ea3e9c12ca18d1f3bf06199f7d72ce52e68f2a1ebcfd8acff9c472/bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db", size = 272242 },
+]
+
 [[package]]
 name = "beautifulsoup4"
 version = "4.12.3"
@@ -561,6 +994,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 },
 ]
 
+[[package]]
+name = "click"
+version = "8.1.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "platform_system == 'Windows'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
+]
+
 [[package]]
 name = "colorama"
 version = "0.4.6"
@@ -579,6 +1024,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9", size = 51068 },
 ]
 
+[[package]]
+name = "configparser"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a5/2e/a8d83652990ecb5df54680baa0c53d182051d9e164a25baa0582363841d1/configparser-7.1.0.tar.gz", hash = "sha256:eb82646c892dbdf773dae19c633044d163c3129971ae09b49410a303b8e0a5f7", size = 50122 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ee/df/1514580907b0bac0970415e5e24ef96a9c1fa71dcf2aa0139045b58fae9a/configparser-7.1.0-py3-none-any.whl", hash = "sha256:98e374573c4e10e92399651e3ba1c47a438526d633c44ee96143dec26dad4299", size = 17074 },
+]
+
 [[package]]
 name = "constantly"
 version = "23.10.4"
@@ -684,6 +1138,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/20/8d/778b7d51b981a96554f29136cd59ca7880bf58094338085bcf2a979a0e6a/Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", size = 9561 },
 ]
 
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
+]
+
 [[package]]
 name = "django"
 version = "5.1.2"
@@ -1001,6 +1464,53 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/76/0f/d8a8152e720cbcad890e56ee98639ff489f1992869b4cf304c3fa24d4bcc/ftfy-6.3.0-py3-none-any.whl", hash = "sha256:17aca296801f44142e3ff2c16f93fbf6a87609ebb3704a9a41dd5d4903396caf", size = 44778 },
 ]
 
+[[package]]
+name = "gevent"
+version = "24.10.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cffi", marker = "platform_python_implementation == 'CPython' and sys_platform == 'win32'" },
+    { name = "greenlet", marker = "platform_python_implementation == 'CPython'" },
+    { name = "zope-event" },
+    { name = "zope-interface" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/70/f0/be10ed5d7721ed2317d7feb59e167603217156c2a6d57f128523e24e673d/gevent-24.10.3.tar.gz", hash = "sha256:aa7ee1bd5cabb2b7ef35105f863b386c8d5e332f754b60cfc354148bd70d35d1", size = 6108837 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/6b/6f/a2100e7883c7bdfc2b45cb60b310ca748762a21596258b9dd01c5c093dbc/gevent-24.10.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d7a1ad0f2da582f5bd238bca067e1c6c482c30c15a6e4d14aaa3215cbb2232f3", size = 3014382 },
+    { url = "https://files.pythonhosted.org/packages/7a/b1/460e4884ed6185d9eb9c4c2e9639d2b254197e46513301c0f63dec22dc90/gevent-24.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4e526fdc279c655c1e809b0c34b45844182c2a6b219802da5e411bd2cf5a8ad", size = 4853460 },
+    { url = "https://files.pythonhosted.org/packages/ca/f6/7ded98760d381229183ecce8db2edcce96f13e23807d31a90c66dae85304/gevent-24.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57a5c4e0bdac482c5f02f240d0354e61362df73501ef6ebafce8ef635cad7527", size = 4977636 },
+    { url = "https://files.pythonhosted.org/packages/7d/21/7b928e6029eedb93ef94fc0aee701f497af2e601f0ec00aac0e72e3f450e/gevent-24.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d67daed8383326dc8b5e58d88e148d29b6b52274a489e383530b0969ae7b9cb9", size = 5058031 },
+    { url = "https://files.pythonhosted.org/packages/00/98/12c03fd004fbeeca01276ffc589f5a368fd741d02582ab7006d1bdef57e7/gevent-24.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e24ffea72e27987979c009536fd0868e52239b44afe6cf7135ce8aafd0f108e", size = 6683694 },
+    { url = "https://files.pythonhosted.org/packages/64/4c/ea14d971452d3da09e49267e052d8312f112c7835120aed78d22ef14efee/gevent-24.10.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c1d80090485da1ea3d99205fe97908b31188c1f4857f08b333ffaf2de2e89d18", size = 5286063 },
+    { url = "https://files.pythonhosted.org/packages/39/3f/397efff27e637d7306caa00d1560512c44028c25c70be1e72c46b79b1b66/gevent-24.10.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f0c129f81d60cda614acb4b0c5731997ca05b031fb406fcb58ad53a7ade53b13", size = 6817462 },
+    { url = "https://files.pythonhosted.org/packages/aa/5d/19939eaa7c5b7c0f37e0a0665a911ddfe1e35c25c512446fc356a065c16e/gevent-24.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:26ca7a6b42d35129617025ac801135118333cad75856ffc3217b38e707383eba", size = 1566631 },
+    { url = "https://files.pythonhosted.org/packages/6e/01/1be5cf013826d8baae235976d6a94f3628014fd2db7c071aeec13f82b4d1/gevent-24.10.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:68c3a0d8402755eba7f69022e42e8021192a721ca8341908acc222ea597029b6", size = 2966909 },
+    { url = "https://files.pythonhosted.org/packages/fe/3e/7fa9ab023f24d8689e2c77951981f8ea1f25089e0349a0bf8b35ee9b9277/gevent-24.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d850a453d66336272be4f1d3a8126777f3efdaea62d053b4829857f91e09755", size = 4913247 },
+    { url = "https://files.pythonhosted.org/packages/db/63/6e40eaaa3c2abd1561faff11dc3e6781f8c25e975354b8835762834415af/gevent-24.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8e58ee3723f1fbe07d66892f1caa7481c306f653a6829b6fd16cb23d618a5915", size = 5049036 },
+    { url = "https://files.pythonhosted.org/packages/94/89/158bc32cdc898dda0481040ac18650022e73133d93460c5af56ca622fe9a/gevent-24.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b52382124eca13135a3abe4f65c6bd428656975980a48e51b17aeab68bdb14db", size = 5107299 },
+    { url = "https://files.pythonhosted.org/packages/64/91/1abe62ee350fdfac186d33f615d0d3a0b3b140e7ccf23c73547aa0deec44/gevent-24.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ca2266e08f43c0e22c028801dff7d92a0b102ef20e4caeb6a46abfb95f6a328", size = 6819625 },
+    { url = "https://files.pythonhosted.org/packages/92/8b/0b2fe0d36b7c4d463e46cc68eaf6c14488bd7d86cc37e995c64a0ff7d02f/gevent-24.10.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d758f0d4dbf32502ec87bb9b536ca8055090a16f8305f0ada3ce6f34e70f2fd7", size = 5474079 },
+    { url = "https://files.pythonhosted.org/packages/12/7b/9f5abbf0021a50321314f850697e0f46d2e5081168223af2d8544af9d19f/gevent-24.10.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0de6eb3d55c03138fda567d9bfed28487ce5d0928c5107549767a93efdf2be26", size = 6901323 },
+    { url = "https://files.pythonhosted.org/packages/8a/63/607715c621ae78ed581b7ba36d076df63feeb352993d521327f865056771/gevent-24.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:385710355eadecdb70428a5ae3e7e5a45dcf888baa1426884588be9d25ac4290", size = 1549468 },
+    { url = "https://files.pythonhosted.org/packages/d9/e4/4edbe17001bb3e6fade4ad2d85ca8f9e4eabcbde4aa29aa6889281616e3e/gevent-24.10.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ad8fb70aa0ebc935729c9699ac31b210a49b689a7b27b7ac9f91676475f3f53", size = 2970952 },
+    { url = "https://files.pythonhosted.org/packages/3c/a6/ce0824fe9398ba6b00028a74840f12be1165d5feaacdc028ea953db3d6c3/gevent-24.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f18689f7a70d2ed0e75bad5036ec3c89690a493d4cfac8d7cdb258ac04b132bd", size = 5172230 },
+    { url = "https://files.pythonhosted.org/packages/25/d4/9002cfb585bfa52c860ed4b1349d1a6400bdf2df9f1bd21df5ff33eea33c/gevent-24.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f4f171d4d2018170454d84c934842e1b5f6ce7468ba298f6e7f7cff15000a3", size = 5338394 },
+    { url = "https://files.pythonhosted.org/packages/0c/98/222f1a14f22ad2d1cbcc37edb74095264c1f9c7ab49e6423693383462b8a/gevent-24.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7021e26d70189b33c27173d4173f27bf4685d6b6f1c0ea50e5335f8491cb110c", size = 5437989 },
+    { url = "https://files.pythonhosted.org/packages/bf/e8/cbb46afea3c7ecdc7289e15cb4a6f89903f4f9754a27ca320d3e465abc78/gevent-24.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34aea15f9c79f27a8faeaa361bc1e72c773a9b54a1996a2ec4eefc8bcd59a824", size = 6838539 },
+    { url = "https://files.pythonhosted.org/packages/69/c3/e43e348f23da404a6d4368a14453ed097cdfca97d5212eaceb987d04a0e1/gevent-24.10.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8af65a4d4feaec6042c666d22c322a310fba3b47e841ad52f724b9c3ce5da48e", size = 5513842 },
+    { url = "https://files.pythonhosted.org/packages/c2/76/84b7c19c072a80900118717a85236859127d630cdf8b079fe42f19649f12/gevent-24.10.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:89c4115e3f5ada55f92b61701a46043fe42f702b5af863b029e4c1a76f6cc2d4", size = 6927374 },
+    { url = "https://files.pythonhosted.org/packages/5e/69/0ab1b04c363547058fb5035275c144957b80b36cb6aee715fe6181b0cee9/gevent-24.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ce6dab94c0b0d24425ba55712de2f8c9cb21267150ca63f5bb3a0e1f165da99", size = 1546701 },
+    { url = "https://files.pythonhosted.org/packages/f7/2d/c783583d7999cd2f2e7aa2d6a1c333d663003ca61255a89ff6a891be95f4/gevent-24.10.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f147e38423fbe96e8731f60a63475b3d2cab2f3d10578d8ee9d10c507c58a2ff", size = 2962857 },
+    { url = "https://files.pythonhosted.org/packages/f3/77/d3ce96fd49406f61976e9a3b6c742b97bb274d3b30c68ff190c5b5f81afd/gevent-24.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e6984ec96fc95fd67488555c38ece3015be1f38b1bcceb27b7d6c36b343008", size = 5141676 },
+    { url = "https://files.pythonhosted.org/packages/49/f4/f99f893770c316b9d2f03bd684947126cbed0321b89fe5423838974c2025/gevent-24.10.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:051b22e2758accfddb0457728bfc9abf8c3f2ce6bca43f1ff6e07b5ed9e49bf4", size = 5310248 },
+    { url = "https://files.pythonhosted.org/packages/e3/0c/67257ba906f76ed82e8f0bd8c00c2a0687b360a1050b70db7e58dff749ab/gevent-24.10.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb5edb6433764119a664bbb148d2aea9990950aa89cc3498f475c2408d523ea3", size = 5407304 },
+    { url = "https://files.pythonhosted.org/packages/35/6c/3a72da7c224b0111728130c0f1abc3ee07feff91b37e0ea83db98f4a3eaf/gevent-24.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce417bcaaab496bc9c77f75566531e9d93816262037b8b2dbb88b0fdcd66587c", size = 6818624 },
+    { url = "https://files.pythonhosted.org/packages/a3/96/cc5f6ecba032a45fc312fe0db2908a893057fd81361eea93845d6c325556/gevent-24.10.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:1c3a828b033fb02b7c31da4d75014a1f82e6c072fc0523456569a57f8b025861", size = 5484356 },
+    { url = "https://files.pythonhosted.org/packages/7c/97/e680b2b2f0c291ae4db9813ffbf02c22c2a0f14c8f1a613971385e29ef67/gevent-24.10.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f2ae3efbbd120cdf4a68b7abc27a37e61e6f443c5a06ec2c6ad94c37cd8471ec", size = 6903191 },
+    { url = "https://files.pythonhosted.org/packages/1b/1c/b4181957da062d1c060974ec6cb798cc24aeeb28e8cd2ece84eb4b4991f7/gevent-24.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:9e1210334a9bc9f76c3d008e0785ca62214f8a54e1325f6c2ecab3b6a572a015", size = 1545117 },
+    { url = "https://files.pythonhosted.org/packages/89/2b/bf4af9950b8f9abd5b4025858f6311930de550e3498bbfeb47c914701a1d/gevent-24.10.3-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:e534e6a968d74463b11de6c9c67f4b4bf61775fb00f2e6e0f7fcdd412ceade18", size = 1271541 },
+]
+
 [[package]]
 name = "googleapis-common-protos"
 version = "1.65.0"
@@ -1013,6 +1523,57 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/ec/08/49bfe7cf737952cc1a9c43e80cc258ed45dad7f183c5b8276fc94cb3862d/googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", size = 220890 },
 ]
 
+[[package]]
+name = "greenlet"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/25/90/5234a78dc0ef6496a6eb97b67a42a8e96742a56f7dc808cb954a85390448/greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563", size = 271235 },
+    { url = "https://files.pythonhosted.org/packages/7c/16/cd631fa0ab7d06ef06387135b7549fdcc77d8d859ed770a0d28e47b20972/greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83", size = 637168 },
+    { url = "https://files.pythonhosted.org/packages/2f/b1/aed39043a6fec33c284a2c9abd63ce191f4f1a07319340ffc04d2ed3256f/greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0", size = 648826 },
+    { url = "https://files.pythonhosted.org/packages/76/25/40e0112f7f3ebe54e8e8ed91b2b9f970805143efef16d043dfc15e70f44b/greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120", size = 644443 },
+    { url = "https://files.pythonhosted.org/packages/fb/2f/3850b867a9af519794784a7eeed1dd5bc68ffbcc5b28cef703711025fd0a/greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc", size = 643295 },
+    { url = "https://files.pythonhosted.org/packages/cf/69/79e4d63b9387b48939096e25115b8af7cd8a90397a304f92436bcb21f5b2/greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617", size = 599544 },
+    { url = "https://files.pythonhosted.org/packages/46/1d/44dbcb0e6c323bd6f71b8c2f4233766a5faf4b8948873225d34a0b7efa71/greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7", size = 1125456 },
+    { url = "https://files.pythonhosted.org/packages/e0/1d/a305dce121838d0278cee39d5bb268c657f10a5363ae4b726848f833f1bb/greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6", size = 1149111 },
+    { url = "https://files.pythonhosted.org/packages/96/28/d62835fb33fb5652f2e98d34c44ad1a0feacc8b1d3f1aecab035f51f267d/greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80", size = 298392 },
+    { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 },
+    { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 },
+    { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 },
+    { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 },
+    { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 },
+    { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 },
+    { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 },
+    { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 },
+    { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 },
+    { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
+    { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
+    { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
+    { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
+    { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
+    { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
+    { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
+    { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
+    { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
+    { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
+    { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
+    { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
+    { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
+    { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
+    { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
+    { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
+    { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
+    { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
+    { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
+    { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
+    { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
+    { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
+    { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
+    { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
+    { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
+]
+
 [[package]]
 name = "h11"
 version = "0.14.0"
@@ -1229,6 +1790,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/a0/9f/5b5481d716670ed5fbd8d06dfa94b7108272b645da2f2406eb909cb6a450/libcst-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d6acb0bdee1e55b44c6215c59755ec4693ac01e74bb1fde04c37358b378835d", size = 2029600 },
 ]
 
+[[package]]
+name = "lockfile"
+version = "0.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/17/47/72cb04a58a35ec495f96984dddb48232b551aafb95bde614605b754fe6f7/lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799", size = 20874 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c8/22/9460e311f340cb62d26a38c419b1381b8593b0bb6b5d1f056938b086d362/lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa", size = 13564 },
+]
+
 [[package]]
 name = "logfire"
 version = "1.2.0"
@@ -1370,36 +1940,36 @@ wheels = [
 
 [[package]]
 name = "mypy"
-version = "1.12.1"
+version = "1.13.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "mypy-extensions" },
     { name = "tomli", marker = "python_full_version < '3.11'" },
     { name = "typing-extensions" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/17/03/744330105a74dc004578f47ec27e1bf66b1dd5664ea444d18423e41343bd/mypy-1.12.1.tar.gz", hash = "sha256:f5b3936f7a6d0e8280c9bdef94c7ce4847f5cdfc258fbb2c29a8c1711e8bb96d", size = 3150767 }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/21/7e9e523537991d145ab8a0a2fd98548d67646dc2aaaf6091c31ad883e7c1/mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e", size = 3152532 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/16/90/3a83d3bcff2eb85151723f116336bd545995b5260a49d3e0d95213fcc2d7/mypy-1.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d7d4371829184e22fda4015278fbfdef0327a4b955a483012bd2d423a788801", size = 11017908 },
-    { url = "https://files.pythonhosted.org/packages/e4/5c/d6b32ddde2460fc63168ca0f7bf44f38474353547f7c0304a30023c40aa0/mypy-1.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f59f1dfbf497d473201356966e353ef09d4daec48caeacc0254db8ef633a28a5", size = 10184164 },
-    { url = "https://files.pythonhosted.org/packages/42/5e/680aa37c938e6db23bd7e6dd4d38d7e609998491721e453b32ec10d31e7f/mypy-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b947097fae68004b8328c55161ac9db7d3566abfef72d9d41b47a021c2fba6b1", size = 12587852 },
-    { url = "https://files.pythonhosted.org/packages/9e/0f/9cafea1c3aaf852cfa1d4a387f33923b6d9714b5c16eb0469da67c5c31e4/mypy-1.12.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:96af62050971c5241afb4701c15189ea9507db89ad07794a4ee7b4e092dc0627", size = 13106489 },
-    { url = "https://files.pythonhosted.org/packages/ea/c3/7f56d5d87a81e665de8dfa424120ab3a6954ae5854946cec0a46f78f6168/mypy-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:d90da248f4c2dba6c44ddcfea94bb361e491962f05f41990ff24dbd09969ce20", size = 9634753 },
-    { url = "https://files.pythonhosted.org/packages/18/0a/70de7c97a86cb85535077ab5cef1cbc4e2812fd2e9cc21d78eb561a6b80f/mypy-1.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1230048fec1380faf240be6385e709c8570604d2d27ec6ca7e573e3bc09c3735", size = 10940998 },
-    { url = "https://files.pythonhosted.org/packages/c0/97/9ed6d4834d7549936ab88533b302184fb568a0940c4000d2aaee6dc07112/mypy-1.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02dcfe270c6ea13338210908f8cadc8d31af0f04cee8ca996438fe6a97b4ec66", size = 10108523 },
-    { url = "https://files.pythonhosted.org/packages/48/41/1686f37d09c915dfc5b683e20cc99dabac199900b5ca6d22747b99ddcb50/mypy-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5a437c9102a6a252d9e3a63edc191a3aed5f2fcb786d614722ee3f4472e33f6", size = 12505553 },
-    { url = "https://files.pythonhosted.org/packages/8d/2b/2dbcaa7e97b23f27ced77493256ee878f4a140ac750e198630ff1b9b60c6/mypy-1.12.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:186e0c8346efc027ee1f9acf5ca734425fc4f7dc2b60144f0fbe27cc19dc7931", size = 12988634 },
-    { url = "https://files.pythonhosted.org/packages/54/55/710d082e91a2ccaea21214229b11f9215a9d22446f949491b5457655e82b/mypy-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:673ba1140a478b50e6d265c03391702fa11a5c5aff3f54d69a62a48da32cb811", size = 9630747 },
-    { url = "https://files.pythonhosted.org/packages/8a/74/b9e0e4f06e951e277058f878302faa154d282ca11274c59fe08353f52949/mypy-1.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9fb83a7be97c498176fb7486cafbb81decccaef1ac339d837c377b0ce3743a7f", size = 11079902 },
-    { url = "https://files.pythonhosted.org/packages/9f/62/fcad290769db3eb0de265094cef5c94d6075c70bc1e42b67eee4ca192dcc/mypy-1.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:389e307e333879c571029d5b93932cf838b811d3f5395ed1ad05086b52148fb0", size = 10072373 },
-    { url = "https://files.pythonhosted.org/packages/cb/27/9ac78349c2952e4446288ec1174675ab9e0160ed18c2cb1154fa456c54e8/mypy-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b2048a95a21f7a9ebc9fbd075a4fcd310410d078aa0228dbbad7f71335e042", size = 12589779 },
-    { url = "https://files.pythonhosted.org/packages/7c/4a/58cebd122cf1cba95680ac51303fbeb508392413ca64e3e711aa7d4877aa/mypy-1.12.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ee5932370ccf7ebf83f79d1c157a5929d7ea36313027b0d70a488493dc1b179", size = 13044459 },
-    { url = "https://files.pythonhosted.org/packages/5b/c7/672935e2a3f9bcc07b1b870395a653f665657bef3cdaa504ad99f56eadf0/mypy-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:19bf51f87a295e7ab2894f1d8167622b063492d754e69c3c2fed6563268cb42a", size = 9731919 },
-    { url = "https://files.pythonhosted.org/packages/bb/b0/092be5094840a401940c95224f63bb2a8f09bce9251ac1df180ec523830c/mypy-1.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d34167d43613ffb1d6c6cdc0cc043bb106cac0aa5d6a4171f77ab92a3c758bcc", size = 11068611 },
-    { url = "https://files.pythonhosted.org/packages/9a/86/f20f53b8f062876c39602243d7a59b5cabd6b24315d8de511d607fa4de6a/mypy-1.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:427878aa54f2e2c5d8db31fa9010c599ed9f994b3b49e64ae9cd9990c40bd635", size = 10068036 },
-    { url = "https://files.pythonhosted.org/packages/84/c7/1dbd6575785522da1d4c1ac2c419505fcf23bee74811880cac447a4a77ab/mypy-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fcde63ea2c9f69d6be859a1e6dd35955e87fa81de95bc240143cf00de1f7f81", size = 12585671 },
-    { url = "https://files.pythonhosted.org/packages/46/8a/f6ae18b446eb2bccce54c4bd94065bcfe417d6c67021dcc032bf1e720aff/mypy-1.12.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d54d840f6c052929f4a3d2aab2066af0f45a020b085fe0e40d4583db52aab4e4", size = 13036083 },
-    { url = "https://files.pythonhosted.org/packages/59/e6/fc65fde3dc7156fce8d49ba21c7b1f5d866ad50467bf196ca94a7f6d2c9e/mypy-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:20db6eb1ca3d1de8ece00033b12f793f1ea9da767334b7e8c626a4872090cf02", size = 9735467 },
-    { url = "https://files.pythonhosted.org/packages/84/6b/1db9de4e0764778251fb2d64cb7455cf6db75dc99c9f72c8b7e74b6a8a17/mypy-1.12.1-py3-none-any.whl", hash = "sha256:ce561a09e3bb9863ab77edf29ae3a50e65685ad74bba1431278185b7e5d5486e", size = 2646060 },
+    { url = "https://files.pythonhosted.org/packages/5e/8c/206de95a27722b5b5a8c85ba3100467bd86299d92a4f71c6b9aa448bfa2f/mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a", size = 11020731 },
+    { url = "https://files.pythonhosted.org/packages/ab/bb/b31695a29eea76b1569fd28b4ab141a1adc9842edde080d1e8e1776862c7/mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80", size = 10184276 },
+    { url = "https://files.pythonhosted.org/packages/a5/2d/4a23849729bb27934a0e079c9c1aad912167d875c7b070382a408d459651/mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7", size = 12587706 },
+    { url = "https://files.pythonhosted.org/packages/5c/c3/d318e38ada50255e22e23353a469c791379825240e71b0ad03e76ca07ae6/mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f", size = 13105586 },
+    { url = "https://files.pythonhosted.org/packages/4a/25/3918bc64952370c3dbdbd8c82c363804678127815febd2925b7273d9482c/mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372", size = 9632318 },
+    { url = "https://files.pythonhosted.org/packages/d0/19/de0822609e5b93d02579075248c7aa6ceaddcea92f00bf4ea8e4c22e3598/mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d", size = 10939027 },
+    { url = "https://files.pythonhosted.org/packages/c8/71/6950fcc6ca84179137e4cbf7cf41e6b68b4a339a1f5d3e954f8c34e02d66/mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d", size = 10108699 },
+    { url = "https://files.pythonhosted.org/packages/26/50/29d3e7dd166e74dc13d46050b23f7d6d7533acf48f5217663a3719db024e/mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b", size = 12506263 },
+    { url = "https://files.pythonhosted.org/packages/3f/1d/676e76f07f7d5ddcd4227af3938a9c9640f293b7d8a44dd4ff41d4db25c1/mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73", size = 12984688 },
+    { url = "https://files.pythonhosted.org/packages/9c/03/5a85a30ae5407b1d28fab51bd3e2103e52ad0918d1e68f02a7778669a307/mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca", size = 9626811 },
+    { url = "https://files.pythonhosted.org/packages/fb/31/c526a7bd2e5c710ae47717c7a5f53f616db6d9097caf48ad650581e81748/mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5", size = 11077900 },
+    { url = "https://files.pythonhosted.org/packages/83/67/b7419c6b503679d10bd26fc67529bc6a1f7a5f220bbb9f292dc10d33352f/mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e", size = 10074818 },
+    { url = "https://files.pythonhosted.org/packages/ba/07/37d67048786ae84e6612575e173d713c9a05d0ae495dde1e68d972207d98/mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2", size = 12589275 },
+    { url = "https://files.pythonhosted.org/packages/1f/17/b1018c6bb3e9f1ce3956722b3bf91bff86c1cefccca71cec05eae49d6d41/mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0", size = 13037783 },
+    { url = "https://files.pythonhosted.org/packages/cb/32/cd540755579e54a88099aee0287086d996f5a24281a673f78a0e14dba150/mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2", size = 9726197 },
+    { url = "https://files.pythonhosted.org/packages/11/bb/ab4cfdc562cad80418f077d8be9b4491ee4fb257440da951b85cbb0a639e/mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7", size = 11069721 },
+    { url = "https://files.pythonhosted.org/packages/59/3b/a393b1607cb749ea2c621def5ba8c58308ff05e30d9dbdc7c15028bca111/mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62", size = 10063996 },
+    { url = "https://files.pythonhosted.org/packages/d1/1f/6b76be289a5a521bb1caedc1f08e76ff17ab59061007f201a8a18cc514d1/mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8", size = 12584043 },
+    { url = "https://files.pythonhosted.org/packages/a6/83/5a85c9a5976c6f96e3a5a7591aa28b4a6ca3a07e9e5ba0cec090c8b596d6/mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7", size = 13036996 },
+    { url = "https://files.pythonhosted.org/packages/b4/59/c39a6f752f1f893fccbcf1bdd2aca67c79c842402b5283563d006a67cf76/mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc", size = 9737709 },
+    { url = "https://files.pythonhosted.org/packages/3b/86/72ce7f57431d87a7ff17d442f521146a6585019eb8f4f31b7c02801f78ad/mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a", size = 2647043 },
 ]
 
 [[package]]
@@ -1606,6 +2176,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
 ]
 
+[[package]]
+name = "paramiko"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "bcrypt" },
+    { name = "cryptography" },
+    { name = "pynacl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1b/0f/c00296e36ff7485935b83d466c4f2cf5934b84b0ad14e81796e1d9d3609b/paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124", size = 1704305 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1f/66/14b2c030fcce69cba482d205c2d1462ca5c77303a263260dcb1192801c85/paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9", size = 227143 },
+]
+
 [[package]]
 name = "parso"
 version = "0.8.4"
@@ -1629,11 +2213,11 @@ wheels = [
 
 [[package]]
 name = "phonenumbers"
-version = "8.13.47"
+version = "8.13.48"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ae/0c/8f315d5e6ddea2e45ae13ada6936df6240858929881daf20cb3133fdb729/phonenumbers-8.13.47.tar.gz", hash = "sha256:53c5e7c6d431cafe4efdd44956078404ae9bc8b0eacc47be3105d3ccc88aaffa", size = 2297081 }
+sdist = { url = "https://files.pythonhosted.org/packages/61/59/d01506a791481d26a640acb0a1124e3f0a816b0711e563962d7d55184890/phonenumbers-8.13.48.tar.gz", hash = "sha256:62d8df9b0f3c3c41571c6b396f044ddd999d61631534001b8be7fdf7ba1b18f3", size = 2297098 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/b6/0b/5cde445764ac72460748107e999b026b7245e3fcc5fd5551cc5aff45e469/phonenumbers-8.13.47-py2.py3-none-any.whl", hash = "sha256:5d3c0142ef7055ca5551884352e3b6b93bfe002a0bc95b8eaba39b0e2184541b", size = 2582530 },
+    { url = "https://files.pythonhosted.org/packages/98/f4/a9340f98335ae6fab1ad4b56b6a04f390de65bea371c71b0cdf67e4c08d0/phonenumbers-8.13.48-py2.py3-none-any.whl", hash = "sha256:5c51939acefa390eb74119750afb10a85d3c628dc83fd62c52d6f532fcf5d205", size = 2582542 },
 ]
 
 [[package]]
@@ -1881,16 +2465,41 @@ wheels = [
 [[package]]
 name = "pydantic-pkgr"
 version = "0.5.4"
-source = { registry = "https://pypi.org/simple" }
+source = { editable = "packages/pydantic-pkgr" }
 dependencies = [
     { name = "platformdirs" },
     { name = "pydantic" },
     { name = "pydantic-core" },
     { name = "typing-extensions" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/d2/18/3bf29e213c4a19d5b08e0fa1048c72f76c54565a208cced1fd4a60f989fc/pydantic_pkgr-0.5.4.tar.gz", hash = "sha256:e3487b46357b1e1b729363385590355cfac261b18ed207f59e9b613c5a8d45b2", size = 42408 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/01/97/9ec8d45e4af1a3af7d0ca78e12bcb1d74a446399034cb1514aab2bac056e/pydantic_pkgr-0.5.4-py3-none-any.whl", hash = "sha256:46ad1ad5954ee9c55b2c2f2c2be749a39992a89edde624454e63d8a7b550be8b", size = 45061 },
+
+[package.optional-dependencies]
+all = [
+    { name = "ansible" },
+    { name = "ansible-core" },
+    { name = "ansible-runner" },
+    { name = "pyinfra" },
+]
+ansible = [
+    { name = "ansible" },
+    { name = "ansible-core" },
+    { name = "ansible-runner" },
+]
+pyinfra = [
+    { name = "pyinfra" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "ansible", marker = "extra == 'ansible'", specifier = ">=10.5.0" },
+    { name = "ansible-core", marker = "extra == 'ansible'", specifier = ">=2.17.5" },
+    { name = "ansible-runner", marker = "extra == 'ansible'", specifier = ">=2.4.0" },
+    { name = "platformdirs", specifier = ">=4.3.6" },
+    { name = "pydantic", specifier = ">=2.7.1" },
+    { name = "pydantic-core", specifier = ">=2.18.2" },
+    { name = "pydantic-pkgr", extras = ["pyinfra", "ansible"], marker = "extra == 'all'", editable = "packages/pydantic-pkgr" },
+    { name = "pyinfra", marker = "extra == 'pyinfra'", specifier = ">=2.6.1" },
+    { name = "typing-extensions", specifier = ">=4.11.0" },
 ]
 
 [[package]]
@@ -1924,6 +2533,49 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f7/3f/01c8b82017c199075f8f788d0d906b9ffbbc5a47dc9918a945e13d5a2bda/pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a", size = 1205513 },
 ]
 
+[[package]]
+name = "pyinfra"
+version = "3.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "configparser" },
+    { name = "distro" },
+    { name = "gevent" },
+    { name = "jinja2" },
+    { name = "packaging" },
+    { name = "paramiko" },
+    { name = "python-dateutil" },
+    { name = "pywinrm" },
+    { name = "setuptools" },
+    { name = "typeguard" },
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/12/1c/bb923dcd1ee29272e31986ef5f64e91b586a0c685efe82672f6cf468e96d/pyinfra-3.1.1.tar.gz", hash = "sha256:5209a05897597c8747511bb559809a64a84377ae77424d3869d46583f95f2f30", size = 198499 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a2/56/cf53e42877039d13c3e07d63a38ce28e2cc4dca167a2cdc5420f2766f95a/pyinfra-3.1.1-py2.py3-none-any.whl", hash = "sha256:c87c75fcc03197ce84cb078838e225669be5cc0c4d4e52e408a9e774a3d183f6", size = 255376 },
+]
+
+[[package]]
+name = "pynacl"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a7/22/27582568be639dfe22ddb3902225f91f2f17ceff88ce80e4db396c8986da/PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba", size = 3392854 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ce/75/0b8ede18506041c0bf23ac4d8e2971b4161cd6ce630b177d0a08eb0d8857/PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1", size = 349920 },
+    { url = "https://files.pythonhosted.org/packages/59/bb/fddf10acd09637327a97ef89d2a9d621328850a72f1fdc8c08bdf72e385f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92", size = 601722 },
+    { url = "https://files.pythonhosted.org/packages/5d/70/87a065c37cca41a75f2ce113a5a2c2aa7533be648b184ade58971b5f7ccc/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394", size = 680087 },
+    { url = "https://files.pythonhosted.org/packages/ee/87/f1bb6a595f14a327e8285b9eb54d41fef76c585a0edef0a45f6fc95de125/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d", size = 856678 },
+    { url = "https://files.pythonhosted.org/packages/66/28/ca86676b69bf9f90e710571b67450508484388bfce09acf8a46f0b8c785f/PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858", size = 1133660 },
+    { url = "https://files.pythonhosted.org/packages/3d/85/c262db650e86812585e2bc59e497a8f59948a005325a11bbbc9ecd3fe26b/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b", size = 663824 },
+    { url = "https://files.pythonhosted.org/packages/fd/1a/cc308a884bd299b651f1633acb978e8596c71c33ca85e9dc9fa33a5399b9/PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff", size = 1117912 },
+    { url = "https://files.pythonhosted.org/packages/25/2d/b7df6ddb0c2a33afdb358f8af6ea3b8c4d1196ca45497dd37a56f0c122be/PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543", size = 204624 },
+    { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141 },
+]
+
 [[package]]
 name = "pyopenssl"
 version = "24.2.1"
@@ -1936,6 +2588,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d9/dd/e0aa7ebef5168c75b772eda64978c597a9129b46be17779054652a7999e4/pyOpenSSL-24.2.1-py3-none-any.whl", hash = "sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d", size = 58390 },
 ]
 
+[[package]]
+name = "pyspnego"
+version = "0.11.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cryptography" },
+    { name = "sspilib", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/46/f5/1f938a781742d18475ac43a101ec8a9499e1655da0984e08b59e20012c04/pyspnego-0.11.1.tar.gz", hash = "sha256:e92ed8b0a62765b9d6abbb86a48cf871228ddb97678598dc01c9c39a626823f6", size = 225697 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/c3/4dc3d1d029e14bf065f1df9e98e3e503e622de34706a06ab6c3731377e85/pyspnego-0.11.1-py3-none-any.whl", hash = "sha256:129a4294f2c4d681d5875240ef87accc6f1d921e8983737fb0b59642b397951e", size = 130456 },
+]
+
 [[package]]
 name = "pytest"
 version = "8.3.3"
@@ -1995,6 +2660,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/3b/91/832fb3b3a1f62bd2ab4924f6be0c7736c9bc4f84d3b153b74efcf6d4e4a1/python_crontab-3.2.0-py3-none-any.whl", hash = "sha256:82cb9b6a312d41ff66fd3caf3eed7115c28c195bfb50711bc2b4b9592feb9fe5", size = 27351 },
 ]
 
+[[package]]
+name = "python-daemon"
+version = "3.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "lockfile" },
+    { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/54/cd/d62884732e5d6ff6906234169d06338d53e37243c60cf73679c8942f9e42/python_daemon-3.1.0.tar.gz", hash = "sha256:fdb621d7e5f46e74b4de1ad6b0fff6e69cd91b4f219de1476190ebdd0f4781df", size = 61947 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/87/78/09ce91de8b31930c415d7439fa4f9d00d25af57135c16358c0b5b0ae0dea/python_daemon-3.1.0-py3-none-any.whl", hash = "sha256:a66b5896f0aed5807a25c6128268eb496488b1f9c6927c487710049ba16be32a", size = 30899 },
+]
+
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -2065,6 +2743,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
 ]
 
+[[package]]
+name = "pywinrm"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "requests" },
+    { name = "requests-ntlm" },
+    { name = "xmltodict" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/2f/d835c342c4b11e28beaccef74982e7669986c84bf19654c39f53c8b8243c/pywinrm-0.5.0.tar.gz", hash = "sha256:5428eb1e494af7954546cd4ff15c9ef1a30a75e05b25a39fd606cef22201e9f1", size = 40875 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0c/45/4340320145c225387f40ce412de1b209d991c322032e4922cc0a9935fd31/pywinrm-0.5.0-py3-none-any.whl", hash = "sha256:c267046d281de613fc7c8a528cdd261564d9b99bdb7c2926221eff3263b700c8", size = 48182 },
+]
+
 [[package]]
 name = "pyyaml"
 version = "6.0.2"
@@ -2207,6 +2899,20 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
 ]
 
+[[package]]
+name = "requests-ntlm"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "cryptography" },
+    { name = "pyspnego" },
+    { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/15/74/5d4e1815107e9d78c44c3ad04740b00efd1189e5a9ec11e5275b60864e54/requests_ntlm-1.3.0.tar.gz", hash = "sha256:b29cc2462623dffdf9b88c43e180ccb735b4007228a542220e882c58ae56c668", size = 16112 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9e/5d/836b97537a390cf811b0488490c389c5a614f0a93acb23f347bd37a2d914/requests_ntlm-1.3.0-py3-none-any.whl", hash = "sha256:4c7534a7d0e482bb0928531d621be4b2c74ace437e88c5a357ceb7452d25a510", size = 6577 },
+]
+
 [[package]]
 name = "requests-tracker"
 version = "0.3.3"
@@ -2220,18 +2926,27 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/16/f5/d2fd9443c1839edf0c17216e9ab03201c16468e82e2968504fc738cd6917/requests_tracker-0.3.3-py3-none-any.whl", hash = "sha256:31d8924470ceea34be51743142c5248f1bf625d2ff95d1f0dccc2cfe14ecda0b", size = 58078 },
 ]
 
+[[package]]
+name = "resolvelib"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ce/10/f699366ce577423cbc3df3280063099054c23df70856465080798c6ebad6/resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309", size = 21065 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194 },
+]
+
 [[package]]
 name = "rich"
-version = "13.9.2"
+version = "13.9.3"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "markdown-it-py" },
     { name = "pygments" },
     { name = "typing-extensions", marker = "python_full_version < '3.11'" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/aa/9e/1784d15b057b0075e5136445aaea92d23955aad2c93eaede673718a40d95/rich-13.9.2.tar.gz", hash = "sha256:51a2c62057461aaf7152b4d611168f93a9fc73068f8ded2790f29fe2b5366d0c", size = 222843 }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/e9/cf9ef5245d835065e6673781dbd4b8911d352fb770d56cf0879cf11b7ee1/rich-13.9.3.tar.gz", hash = "sha256:bc1e01b899537598cf02579d2b9f4a415104d3fc439313a7a2c165d76557a08e", size = 222889 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/67/91/5474b84e505a6ccc295b2d322d90ff6aa0746745717839ee0c5fb4fdcceb/rich-13.9.2-py3-none-any.whl", hash = "sha256:8c82a3d3f8dcfe9e734771313e606b39d8247bb6b826e196f4914b333b743cf1", size = 242117 },
+    { url = "https://files.pythonhosted.org/packages/9a/e2/10e9819cf4a20bd8ea2f5dabafc2e6bf4a78d6a0965daeb60a4b34d1c11f/rich-13.9.3-py3-none-any.whl", hash = "sha256:9836f5096eb2172c9e77df411c1b009bace4193d6a481d534fea75ebba758283", size = 242157 },
 ]
 
 [[package]]
@@ -2463,6 +3178,26 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/5d/a5/b2860373aa8de1e626b2bdfdd6df4355f0565b47e51f7d0c54fe70faf8fe/sqlparse-0.5.1-py3-none-any.whl", hash = "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", size = 44156 },
 ]
 
+[[package]]
+name = "sspilib"
+version = "0.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/48/8d634ac9aa5404b77f2d66b5a354751b7bbbf2be2947328fe895034cb750/sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8", size = 55815 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/49/ac/b59283a2a0c91ef136f4979d711cd8dcd005b9f18b4a50ffaaa50e00f200/sspilib-0.2.0-cp310-cp310-win32.whl", hash = "sha256:e436fa09bcf353a364a74b3ef6910d936fa8cd1493f136e517a9a7e11b319c57", size = 487673 },
+    { url = "https://files.pythonhosted.org/packages/c5/bc/84cb16b512902b972cfd89130918f01aabb8016814442ff6bd2cf89d6530/sspilib-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:850a17c98d2b8579b183ce37a8df97d050bc5b31ab13f5a6d9e39c9692fe3754", size = 565326 },
+    { url = "https://files.pythonhosted.org/packages/c5/0d/d15fe0e5c87a51b7d693e889656816fd8d67995fbd072ab9852934e9ecf4/sspilib-0.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:a4d788a53b8db6d1caafba36887d5ac2087e6b6be6f01eb48f8afea6b646dbb5", size = 473562 },
+    { url = "https://files.pythonhosted.org/packages/70/16/c31487f432724813a27f30c1a63ec07217adf65572e33fe9c4dcfd47a1b3/sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6", size = 485419 },
+    { url = "https://files.pythonhosted.org/packages/15/e9/0cb63b7f1014eff9c1a5b83920a423080b10f29ddf0264fced6abbdbad28/sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c", size = 564816 },
+    { url = "https://files.pythonhosted.org/packages/b9/d9/3b8295f652afe71c0cdfd731eb7d37cc13a8adbfeacd3d67606d486d79b2/sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc", size = 472529 },
+    { url = "https://files.pythonhosted.org/packages/a9/82/07a49f00c0e7feff26f288b5f0747add197fc0db1ddddfab5fd5bdd94bdf/sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8", size = 487318 },
+    { url = "https://files.pythonhosted.org/packages/38/54/949a9e9c07cd6efead79a7f78cc951cb5fa4f9f1fcb25b8520fd2adcdbe0/sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079", size = 569220 },
+    { url = "https://files.pythonhosted.org/packages/8f/52/c7a16472e9582474626f48ec79a821f66e5698cf5552baf923dfc636989e/sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36", size = 471371 },
+    { url = "https://files.pythonhosted.org/packages/bc/9c/8784d3afe27c2f68620ea60fa2b6347100694db35193ba42714bdf23f882/sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92", size = 483600 },
+    { url = "https://files.pythonhosted.org/packages/49/ad/40f898075c913c75060c17c9cc6d6b86e8f83b6f5e1e017627b07ff53fcd/sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa", size = 563678 },
+    { url = "https://files.pythonhosted.org/packages/dd/84/3232ee82e33e426cd9e2011111a3136e5715428f0331a6739930b530333a/sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e", size = 469030 },
+]
+
 [[package]]
 name = "stack-data"
 version = "0.6.3"
@@ -2559,6 +3294,18 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/7d/6c/a53cc9a97c2da76d9cd83c03f377468599a28f2d4ad9fc71c3b99640e71e/txaio-23.1.1-py2.py3-none-any.whl", hash = "sha256:aaea42f8aad50e0ecfb976130ada140797e9dcb85fad2cf72b0f37f8cefcb490", size = 30512 },
 ]
 
+[[package]]
+name = "typeguard"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8d/e1/3178b3e5369a98239ed7301e3946747048c66f4023163d55918f11b82d4e/typeguard-4.3.0.tar.gz", hash = "sha256:92ee6a0aec9135181eae6067ebd617fd9de8d75d714fb548728a4933b1dea651", size = 73374 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/eb/de/be0ba39ee73760bf33329b7c6f95bc67e96593c69c881671e312538e24bb/typeguard-4.3.0-py3-none-any.whl", hash = "sha256:4d24c5b39a117f8a895b9da7a9b3114f04eb63bade45a4492de49b175b6f7dfa", size = 35385 },
+]
+
 [[package]]
 name = "typeid-python"
 version = "0.3.1"
@@ -2639,27 +3386,27 @@ wheels = [
 
 [[package]]
 name = "uv"
-version = "0.4.25"
+version = "0.4.26"
 source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d0/bc/1a013408b7f9f437385705652f404b6b15127ecf108327d13be493bdfb81/uv-0.4.25.tar.gz", hash = "sha256:d39077cdfe3246885fcdf32e7066ae731a166101d063629f9cea08738f79e6a3", size = 2064863 }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/90/500da91a6d2fdad8060d27b0c2dd948bb807a7cfc5fe32abc90dfaeb363f/uv-0.4.26.tar.gz", hash = "sha256:e9f45d8765a037a13ddedebb9e36fdcf06b7957654cfa8055d84f19eba12957e", size = 2072287 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/84/18/9c9056d373620b1cf5182ce9b2d258e86d117d667cf8883e12870f2a5edf/uv-0.4.25-py3-none-linux_armv6l.whl", hash = "sha256:94fb2b454afa6bdfeeea4b4581c878944ca9cf3a13712e6762f245f5fbaaf952", size = 13028246 },
-    { url = "https://files.pythonhosted.org/packages/a1/19/8a3f09aba30ac5433dfecde55d5241a07c96bb12340c3b810bc58188a12e/uv-0.4.25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a7c3a18c20ddb527d296d1222bddf42b78031c50b5b4609d426569b5fb61f5b0", size = 13175265 },
-    { url = "https://files.pythonhosted.org/packages/e8/c9/2f924bb29bd53c51b839c1c6126bd2cf4c451d4a7d8f34be078f9e31c57e/uv-0.4.25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:18100f0f36419a154306ed6211e3490bf18384cdf3f1a0950848bf64b62fa251", size = 12255610 },
-    { url = "https://files.pythonhosted.org/packages/b2/5a/d8f8971aeb3389679505cf633a786cd72a96ce232f80f14cfe5a693b4c64/uv-0.4.25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:6e981b1465e30102e41946adede9cb08051a5d70c6daf09f91a7ea84f0b75c08", size = 12506511 },
-    { url = "https://files.pythonhosted.org/packages/e3/96/8c73520daeba5022cec8749e44afd4ca9ef774bf728af9c258bddec3577f/uv-0.4.25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:578ae385fad6bd6f3868828e33d54994c716b315b1bc49106ec1f54c640837e4", size = 12836250 },
-    { url = "https://files.pythonhosted.org/packages/67/3d/b0e810d365fb154fe1d380a0f43ee35a683cf9162f2501396d711bec2621/uv-0.4.25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d29a78f011ecc2f31c13605acb6574c2894c06d258b0f8d0dbb899986800450", size = 13521303 },
-    { url = "https://files.pythonhosted.org/packages/2d/f4/dd3830ec7fc6e7e5237c184f30f2dbfed4f93605e472147eca1373bcc72b/uv-0.4.25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec181be2bda10651a3558156409ac481549983e0276d0e3645e3b1464e7f8715", size = 14105308 },
-    { url = "https://files.pythonhosted.org/packages/f4/4e/0fca02f8681e4870beda172552e747e0424f6e9186546b00a5e92525fea9/uv-0.4.25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50c7d0d9e7f392f81b13bf3b7e37768d1486f2fc9d533a54982aa0ed11e4db23", size = 13859475 },
-    { url = "https://files.pythonhosted.org/packages/33/07/1100e9bc652f2850930f466869515d16ffe9582aaaaa99bac332ebdfe3ea/uv-0.4.25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fc35b5273f1e018aecd66b70e0fd7d2eb6698853dde3e2fc644e7ebf9f825b1", size = 18100840 },
-    { url = "https://files.pythonhosted.org/packages/fa/98/ba1cb7dd2aa639a064a9e49721e08f12a3424456d60dde1327e7c6437930/uv-0.4.25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7022a71ff63a3838796f40e954b76bf7820fc27e96fe002c537e75ff8e34f1d", size = 13645464 },
-    { url = "https://files.pythonhosted.org/packages/0d/05/b97fb8c828a070e8291826922b2712d1146b11563b4860bc9ba80f5635d1/uv-0.4.25-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e02afb0f6d4b58718347f7d7cfa5a801e985ce42181ba971ed85ef149f6658ca", size = 12694995 },
-    { url = "https://files.pythonhosted.org/packages/b3/97/63df050811379130202898f60e735a1a331ba3a93b8aa1e9bb466f533913/uv-0.4.25-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:3d7680795ea78cdbabbcce73d039b2651cf1fa635ddc1aa3082660f6d6255c50", size = 12831737 },
-    { url = "https://files.pythonhosted.org/packages/dc/e0/08352dcffa6e8435328861ea60b2c05e8bd030f1e93998443ba66209db7b/uv-0.4.25-py3-none-musllinux_1_1_i686.whl", hash = "sha256:aae9dcafd20d5ba978c8a4939ab942e8e2e155c109e9945207fbbd81d2892c9e", size = 13273529 },
-    { url = "https://files.pythonhosted.org/packages/25/f4/eaf95e5eee4e2e69884df0953d094deae07216f72068ef1df08c0f49841d/uv-0.4.25-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:4c55040e67470f2b73e95e432aba06f103a0b348ea0b9c6689b1029c8d9e89fd", size = 15039860 },
-    { url = "https://files.pythonhosted.org/packages/69/04/482b1cc9e8d599c7d766c4ba2d7a512ed3989921443792f92f26b8d44fe6/uv-0.4.25-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bdbfd0c476b9e80a3f89af96aed6dd7d2782646311317a9c72614ccce99bb2ad", size = 13776302 },
-    { url = "https://files.pythonhosted.org/packages/cd/7e/3d1cb735cc3df6341ac884b73eeec1f51a29192721be40be8e9b1d82666d/uv-0.4.25-py3-none-win32.whl", hash = "sha256:7d266e02fefef930609328c31c075084295c3cb472bab3f69549fad4fd9d82b3", size = 12970553 },
-    { url = "https://files.pythonhosted.org/packages/04/e9/c00d2bb4a286b13fad0f06488ea9cbe9e76d0efcd81e7a907f72195d5b83/uv-0.4.25-py3-none-win_amd64.whl", hash = "sha256:be2a4fc4fcade9ea5e67e51738c95644360d6e59b6394b74fc579fb617f902f7", size = 14702875 },
+    { url = "https://files.pythonhosted.org/packages/bf/1f/1e1af6656e83a9b0347c22328ad6d899760819e5f19fa80aee88b56d1e02/uv-0.4.26-py3-none-linux_armv6l.whl", hash = "sha256:d1ca5183afab454f28573a286811019b3552625af2cd1cd3996049d3bbfdb1ca", size = 13055731 },
+    { url = "https://files.pythonhosted.org/packages/92/27/2235628adcf468bc6be98b84e509afa54240d359b4705454e7e957a9650d/uv-0.4.26-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:391a6f5e31b212cb72a8f460493bbdf4088e66049666ad064ac8530230031289", size = 13230933 },
+    { url = "https://files.pythonhosted.org/packages/36/ce/dd9b312c2230705119d3de910a32bbd32dc500bf147c7a0076a31bdfd153/uv-0.4.26-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acaa25b304db6f1e8064d3280532ecb80a58346e37f4199659269847848c4da0", size = 12266060 },
+    { url = "https://files.pythonhosted.org/packages/4d/64/ef6532d84841f5e77e240df9a7dbdc3ca5bf45fae323f247b7bd57bea037/uv-0.4.26-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:2ddb60d508b668b8da055651b30ff56c1efb79d57b064c218a7622b5c74b2af8", size = 12539139 },
+    { url = "https://files.pythonhosted.org/packages/1b/30/b4f98f5e28a8c41e370be1a6ef9d48a619e20d3caeb2bf437f1560fab2df/uv-0.4.26-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6f66f11e088d231b7e305f089dc949b0e6b1d65e0a877b50ba5c3ae26e151144", size = 12867987 },
+    { url = "https://files.pythonhosted.org/packages/7f/5f/605fe50a0710a78013ad5b2b1034d8f056b5971fc023b6510a24e9350637/uv-0.4.26-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e086ebe200e9718e9622af405d45caad9d84b60824306fcb220335fe6fc90966", size = 13594669 },
+    { url = "https://files.pythonhosted.org/packages/ae/4b/e3d02b963f9f83f76d1b0757204a210aceebe8ae16f69fcb431b09bc3926/uv-0.4.26-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:41f9876c22ad5b4518bffe9e50ec7169e242b64f139cdcaf42a76f70a9bd5c78", size = 14156314 },
+    { url = "https://files.pythonhosted.org/packages/40/8e/7803d3b76d8694ba939509e49d0c37e70a6d580ef5b7f0242701533920e5/uv-0.4.26-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6091075420eda571b0377d351c393b096514cb036a3199e033e003edaa0ff880", size = 13897243 },
+    { url = "https://files.pythonhosted.org/packages/97/ee/8d5b63b590d3cb9dae5ac396cc099dcad2e368794d77e34a52dd896e5d8e/uv-0.4.26-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1214caacc6b9f9c72749634c7a82a5d93123a44b70a1fa6a9d13993c126ca33e", size = 17961411 },
+    { url = "https://files.pythonhosted.org/packages/da/9a/5a6a3ea6c2bc42904343897b666cb8c9ac921bf9551b463aeb592cd49d45/uv-0.4.26-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a63a6fe6f249a9fff72328204c3e6b457aae5914590e6881b9b39dcc72d24df", size = 13700388 },
+    { url = "https://files.pythonhosted.org/packages/33/52/009ea704318c5d0f290fb2ea4e1874d5625a60b290c6e5e49aae4d140091/uv-0.4.26-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c4c69532cb4d0c1e160883142b8bf0133a5a67e9aed5148e13743ae55c2dfc03", size = 12702036 },
+    { url = "https://files.pythonhosted.org/packages/72/38/4dc590872e5c1810c6ec203d9b070278ed396a1ebf3396e556079946c894/uv-0.4.26-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:9560c2eb234ea92276bbc647854d4a9e75556981c1193c3cc59f6613f7d177f2", size = 12854127 },
+    { url = "https://files.pythonhosted.org/packages/76/73/124820b37d1c8784fbebfc4b5b7812b4fa8e4e680c35b77a38be444dac9f/uv-0.4.26-py3-none-musllinux_1_1_i686.whl", hash = "sha256:a41bdd09b9a3ddc8f459c73e924485e1caae43e43305cedb65f5feac05cf184a", size = 13309009 },
+    { url = "https://files.pythonhosted.org/packages/f4/e7/37cf24861c6f76ba85ac80c15c391848524668be8dcd218ed04da80a96b6/uv-0.4.26-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:23cee82020b9e973a5feba81c2cf359a5a09020216d98534926f45ee7b74521d", size = 15079442 },
+    { url = "https://files.pythonhosted.org/packages/ca/ac/fa29079ee0c26c65efca5c447ef6ce66f0afca1f73c09d599229d2d9dfd4/uv-0.4.26-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:468f806e841229c0bd6e1cffaaffc064720704623890cee15b42b877cef748c5", size = 13827888 },
+    { url = "https://files.pythonhosted.org/packages/40/e8/f9824ecb8b13da5e8b0e9b8fbc81edb9e0d41923ebc6e287ae2e5a04bc62/uv-0.4.26-py3-none-win32.whl", hash = "sha256:70a108399d6c9e3d1f4a0f105d6d016f97f292dbb6c724e1ed2e6dc9f6872c79", size = 13092190 },
+    { url = "https://files.pythonhosted.org/packages/46/91/c76682177dbe46dc0cc9221f9483b186ad3d8e0b59056c2cdae5c011609c/uv-0.4.26-py3-none-win_amd64.whl", hash = "sha256:e826b544020ef407387ed734a89850cac011ee4b5daf94b4f616b71eff2c8a94", size = 14757412 },
 ]
 
 [[package]]
@@ -2862,7 +3609,7 @@ wheels = [
 
 [[package]]
 name = "yt-dlp"
-version = "2024.10.7"
+version = "2024.10.22"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "brotli", marker = "implementation_name == 'cpython'" },
@@ -2874,9 +3621,9 @@ dependencies = [
     { name = "urllib3" },
     { name = "websockets" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/b1/08679efb4c1932dc6420deda8a89f03d7440d6462b7f61d339db2732a497/yt_dlp-2024.10.7.tar.gz", hash = "sha256:0baf1ab517c9748d7e337ced91c5543c36fc16246a9ebedac32ebf20c1998ceb", size = 2877443 }
+sdist = { url = "https://files.pythonhosted.org/packages/2f/79/acfe1c2bf64ed83e1b465e6550c0f5bc2214ea447a900b102f5ca6e4186e/yt_dlp-2024.10.22.tar.gz", hash = "sha256:47b82a1fd22411b5c95ef2f0a1ae1af4e6dfd736ea99fdb2a0ea41445abc62ba", size = 2885622 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/6e/91/ecb07d66110334cdb01e94b187577af3b041897090203c9957728825d46f/yt_dlp-2024.10.7-py3-none-any.whl", hash = "sha256:9e336ae663bfd7ad3ea1c02e722747388172719efc0fc39a807dace3073aa704", size = 3149082 },
+    { url = "https://files.pythonhosted.org/packages/bb/68/548f9819b41d53561d4f3d39588111cf39993c066b6e5300b4ae118eb2e6/yt_dlp-2024.10.22-py3-none-any.whl", hash = "sha256:ba166602ebe22a220e4dc1ead45bf00eb469ed812b22f4fb8bb54734f9b02084", size = 3155189 },
 ]
 
 [[package]]
@@ -2889,36 +3636,48 @@ wheels = [
 ]
 
 [[package]]
-name = "zope-interface"
-version = "7.1.0"
+name = "zope-event"
+version = "5.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "setuptools" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/e4/1f/8bb0739aba9a8909bcfa2e12dc20443ebd5bd773b6796603f1a126211e18/zope_interface-7.1.0.tar.gz", hash = "sha256:3f005869a1a05e368965adb2075f97f8ee9a26c61898a9e52a9764d93774f237", size = 300239 }
+sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/52/cf/6fe78d1748ade8bde9e0afa0b7a6dc53427fa817c44c0c67937f4a3890ca/zope.interface-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2bd9e9f366a5df08ebbdc159f8224904c1c5ce63893984abb76954e6fbe4381a", size = 207992 },
-    { url = "https://files.pythonhosted.org/packages/98/6a/7583a3bf0ba508d7454b69928ced99f516af674be7a2781d681bbdf3e439/zope.interface-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:661d5df403cd3c5b8699ac480fa7f58047a3253b029db690efa0c3cf209993ef", size = 208498 },
-    { url = "https://files.pythonhosted.org/packages/f2/d7/acae0a46ff4494ade2478335aeb2dec2ec024b7761915b82887cb04f207d/zope.interface-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91b6c30689cfd87c8f264acb2fc16ad6b3c72caba2aec1bf189314cf1a84ca33", size = 254730 },
-    { url = "https://files.pythonhosted.org/packages/76/78/42201e0e6150a14d6aaf138f969186a89ec31d25a5860b7c054191cfefa6/zope.interface-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b6a4924f5bad9fe21d99f66a07da60d75696a136162427951ec3cb223a5570d", size = 249135 },
-    { url = "https://files.pythonhosted.org/packages/3f/1e/a2bb69085db973bc936493e1a870c708b4e61496c4c1f04033a9aeb2dcce/zope.interface-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80a3c00b35f6170be5454b45abe2719ea65919a2f09e8a6e7b1362312a872cd3", size = 254254 },
-    { url = "https://files.pythonhosted.org/packages/4f/cf/a5cb40b19f52c100d0ce22797f63ac865ced81fbf3a75a7ae0ecf2c45810/zope.interface-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b936d61dbe29572fd2cfe13e30b925e5383bed1aba867692670f5a2a2eb7b4e9", size = 211705 },
-    { url = "https://files.pythonhosted.org/packages/9a/0b/c9dd45c073109fcaa63d5e167cae9e364fcb25f3626350127258a678ff0f/zope.interface-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ac20581fc6cd7c754f6dff0ae06fedb060fa0e9ea6309d8be8b2701d9ea51c4", size = 208524 },
-    { url = "https://files.pythonhosted.org/packages/e0/34/57afb328bcced4d0472c11cfab5581cc1e6bb91adf1bb87509a4f5690755/zope.interface-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:848b6fa92d7c8143646e64124ed46818a0049a24ecc517958c520081fd147685", size = 209032 },
-    { url = "https://files.pythonhosted.org/packages/e9/a4/b2e4900f6d4a572979b5e8aa95f1ff9296b458978537f51ba546da51c108/zope.interface-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec1ef1fdb6f014d5886b97e52b16d0f852364f447d2ab0f0c6027765777b6667", size = 261251 },
-    { url = "https://files.pythonhosted.org/packages/c3/89/2cd0a6b24819c024b340fa67f0dda65d0ac8bbd81f35a1fa7c468b681d55/zope.interface-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bcff5c09d0215f42ba64b49205a278e44413d9bf9fa688fd9e42bfe472b5f4f", size = 255366 },
-    { url = "https://files.pythonhosted.org/packages/9e/00/e58be3067025ffbeed48094a07c1972d8150f6d628151fde66f16fa0d4ae/zope.interface-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07add15de0cc7e69917f7d286b64d54125c950aeb43efed7a5ea7172f000fbc1", size = 260078 },
-    { url = "https://files.pythonhosted.org/packages/d1/b6/56436f9f6b74c13c9cd3dbd8345f47823d72b7c9ba2b39872cb7bee4cf42/zope.interface-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:9940d5bc441f887c5f375ec62bcf7e7e495a2d5b1da97de1184a88fb567f06af", size = 212092 },
-    { url = "https://files.pythonhosted.org/packages/ee/d7/0ab8291230cf4fa05fa6f7bb26e0206d799a922070bc3a102f88133edc1e/zope.interface-7.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f245d039f72e6f802902375755846f5de1ee1e14c3e8736c078565599bcab621", size = 208649 },
-    { url = "https://files.pythonhosted.org/packages/4e/ce/598d623faeca8a7ccb120a7d94f707efb61d21a57324a905c9a2bdb7b4b9/zope.interface-7.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6159e767d224d8f18deff634a1d3722e68d27488c357f62ebeb5f3e2f5288b1f", size = 209053 },
-    { url = "https://files.pythonhosted.org/packages/ea/d0/c88caffdf6cf99e9b5d1fad9bdfa94d9eee21f72c2f9f4768bced100aab7/zope.interface-7.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e956b1fd7f3448dd5e00f273072e73e50dfafcb35e4227e6d5af208075593c9", size = 266506 },
-    { url = "https://files.pythonhosted.org/packages/1d/bd/2b665bb66b18169828f0e3d0865eabdb3c8f59556db90367950edccfc072/zope.interface-7.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff115ef91c0eeac69cd92daeba36a9d8e14daee445b504eeea2b1c0b55821984", size = 261229 },
-    { url = "https://files.pythonhosted.org/packages/04/a0/9a0595057002784395990b5e5a5e84e71905f5c110ea5ecae469dc831468/zope.interface-7.1.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec001798ab62c3fc5447162bf48496ae9fba02edc295a9e10a0b0c639a6452e", size = 267167 },
-    { url = "https://files.pythonhosted.org/packages/fb/64/cf1a22aad65dc9746fdc6705042c066011e3fe80f9c73aea9a53b0b3642d/zope.interface-7.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:124149e2d42067b9c6597f4dafdc7a0983d0163868f897b7bb5dc850b14f9a87", size = 212207 },
-    { url = "https://files.pythonhosted.org/packages/43/39/75d4e59474ec7aeb8eebb01fae88e97ee8b0b3144d7a445679f000001977/zope.interface-7.1.0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:9733a9a0f94ef53d7aa64661811b20875b5bc6039034c6e42fb9732170130573", size = 208650 },
-    { url = "https://files.pythonhosted.org/packages/c9/24/929b5530508a39a842fe50e159681b3dd36800604252940662268c3a8551/zope.interface-7.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5fcf379b875c610b5a41bc8a891841533f98de0520287d7f85e25386cd10d3e9", size = 209057 },
-    { url = "https://files.pythonhosted.org/packages/fa/a3/07c120b40d47a3b28faadbacea579db8d7dc9214c909da13d72fd55395f7/zope.interface-7.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0a45b5af9f72c805ee668d1479480ca85169312211bed6ed18c343e39307d5f", size = 266466 },
-    { url = "https://files.pythonhosted.org/packages/4f/fa/e1925c8737787887a2801a45aadbc1ca8367fd9f135e721a2ce5a020e14d/zope.interface-7.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af4a12b459a273b0b34679a5c3dc5e34c1847c3dd14a628aa0668e19e638ea2", size = 261220 },
-    { url = "https://files.pythonhosted.org/packages/d5/79/d7828b915edf77f8f7849e0ab4380084d07c3d09ef86f9763f1490661d66/zope.interface-7.1.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a735f82d2e3ed47ca01a20dfc4c779b966b16352650a8036ab3955aad151ed8a", size = 267157 },
-    { url = "https://files.pythonhosted.org/packages/98/ac/012f18dc9b35e8547975f6e0512bcb6a1e97901d7a5e4e4cb5899dee6304/zope.interface-7.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:5501e772aff595e3c54266bc1bfc5858e8f38974ce413a8f1044aae0f32a83a3", size = 212213 },
+    { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 },
+]
+
+[[package]]
+name = "zope-interface"
+version = "7.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "setuptools" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/f5/1079cab32302359cc09bd1dca9656e680601e0e8af9397322ab0fe85f368/zope.interface-7.1.1.tar.gz", hash = "sha256:4284d664ef0ff7b709836d4de7b13d80873dc5faeffc073abdb280058bfac5e3", size = 253129 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/33/41/328372febe88b50cb1c77d99fd3ee8e628fb125bd26b38b5351f8b9bdcbb/zope.interface-7.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6650bd56ef350d37c8baccfd3ee8a0483ed6f8666e641e4b9ae1a1827b79f9e5", size = 208001 },
+    { url = "https://files.pythonhosted.org/packages/22/06/ced7336eeabba528a39803ccdf52200daa4e7b73d74feac52677f7c83a72/zope.interface-7.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84e87eba6b77a3af187bae82d8de1a7c208c2a04ec9f6bd444fd091b811ad92e", size = 208518 },
+    { url = "https://files.pythonhosted.org/packages/9a/c9/3a63c758a68739080d8c343dda2fca4d214096ed97ce56b875086b309dd2/zope.interface-7.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c4e1b4c06d9abd1037c088dae1566c85f344a3e6ae4350744c3f7f7259d9c67", size = 254689 },
+    { url = "https://files.pythonhosted.org/packages/9a/59/d8c59cfb16b3f086c868d0c531892c3914acbbb324005f0e5c640855a596/zope.interface-7.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cd5e3d910ac87652a09f6e5db8e41bc3b49cf08ddd2d73d30afc644801492cd", size = 249133 },
+    { url = "https://files.pythonhosted.org/packages/9a/6e/449acdd6530cbb9c224be3e59b032d8fc6db35ea8b398aaabcaee50f3881/zope.interface-7.1.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca95594d936ee349620900be5b46c0122a1ff6ce42d7d5cb2cf09dc84071ef16", size = 254250 },
+    { url = "https://files.pythonhosted.org/packages/76/cb/8a13047ae686ca0a478cbf9043132acdcc8ccf71cfa0af287de235fd54f4/zope.interface-7.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:ad339509dcfbbc99bf8e147db6686249c4032f26586699ec4c82f6e5909c9fe2", size = 211708 },
+    { url = "https://files.pythonhosted.org/packages/cc/9e/a53e0b252dca6f4858765efd4287239542e3018efe403ccf4f4947b1f6a8/zope.interface-7.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e59f175e868f856a77c0a77ba001385c377df2104fdbda6b9f99456a01e102a", size = 208535 },
+    { url = "https://files.pythonhosted.org/packages/4a/2c/19bb3ead6133fe457e833af67cc8ce497f54bfd90f5ac532af6e4892acb2/zope.interface-7.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0de23bcb93401994ea00bc5c677ef06d420340ac0a4e9c10d80e047b9ce5af3f", size = 209053 },
+    { url = "https://files.pythonhosted.org/packages/18/3f/3b341ed342f594f3b9e3fc48acecd929d118ee1ea6e415cedfebc2b78214/zope.interface-7.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdb7e7e5524b76d3ec037c1d81a9e2c7457b240fd4cb0a2476b65c3a5a6c81f", size = 260764 },
+    { url = "https://files.pythonhosted.org/packages/65/2a/bb8f72d938cf4edf7e40cbdf14477242a3753205c4f537dafdfbb33249e5/zope.interface-7.1.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3603ef82a9920bd0bfb505423cb7e937498ad971ad5a6141841e8f76d2fd5446", size = 254805 },
+    { url = "https://files.pythonhosted.org/packages/b1/60/abc01b59a41762cf785be8e997a7301e3cb93d19e066a35f10fb31ac0277/zope.interface-7.1.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d52d052355e0c5c89e0630dd2ff7c0b823fd5f56286a663e92444761b35e25", size = 259573 },
+    { url = "https://files.pythonhosted.org/packages/19/50/52a20a6a9e7c605eabb87dcdd5823369d3096854c41b968f2d1e18a8ae8f/zope.interface-7.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:179ad46ece518c9084cb272e4a69d266b659f7f8f48e51706746c2d8a426433e", size = 212067 },
+    { url = "https://files.pythonhosted.org/packages/0f/fe/52bd130dd3f8b88868e741cf9bfeea4367e13d3f84933746f4ba01c85e6b/zope.interface-7.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e6503534b52bb1720ace9366ee30838a58a3413d3e197512f3338c8f34b5d89d", size = 208716 },
+    { url = "https://files.pythonhosted.org/packages/8b/a9/51fe239b07f69384e77568ca3098c518926204eb1fdc7cdcc154c0c78521/zope.interface-7.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f85b290e5b8b11814efb0d004d8ce6c9a483c35c462e8d9bf84abb93e79fa770", size = 209115 },
+    { url = "https://files.pythonhosted.org/packages/f0/fe/33f1f1e68d54c9563db436596a648e57c9dfc298dc0525d348cdb5e812d0/zope.interface-7.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d029fac6a80edae80f79c37e5e3abfa92968fe921886139b3ee470a1b177321a", size = 264001 },
+    { url = "https://files.pythonhosted.org/packages/2e/7f/4d6dafc4debe955a72dd33f8cae1d2e522d43b42167ee8735fd0fe36961e/zope.interface-7.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5836b8fb044c6e75ba34dfaabc602493019eadfa0faf6ff25f4c4c356a71a853", size = 259018 },
+    { url = "https://files.pythonhosted.org/packages/7d/3f/3180bbd9937a2889a67ad2515e56869e0cdb1f47a1f0da52dc1065c81ff8/zope.interface-7.1.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7395f13533318f150ee72adb55b29284b16e73b6d5f02ab21f173b3e83f242b8", size = 264470 },
+    { url = "https://files.pythonhosted.org/packages/95/b8/46a52bfec80089d7e687c1e4471c5918e3a60c2dfff63d3e5588e4bd6656/zope.interface-7.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:1d0e23c6b746eb8ce04573cc47bcac60961ac138885d207bd6f57e27a1431ae8", size = 212226 },
+    { url = "https://files.pythonhosted.org/packages/7e/78/60fb41f6fca56f90a107244e28768deac8697de8cc0f7c8469725c9949ad/zope.interface-7.1.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:9fad9bd5502221ab179f13ea251cb30eef7cf65023156967f86673aff54b53a0", size = 208720 },
+    { url = "https://files.pythonhosted.org/packages/a5/4b/9152d924be141a1b52700ec0bb5c9a28795f67f4253dadb7f4c0c6d63675/zope.interface-7.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:55c373becbd36a44d0c9be1d5271422fdaa8562d158fb44b4192297b3c67096c", size = 209114 },
+    { url = "https://files.pythonhosted.org/packages/00/cc/23d6d94db158b31b82e92202d3e8938d5e5cb38e3141af823a34bd8ae511/zope.interface-7.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed1df8cc01dd1e3970666a7370b8bfc7457371c58ba88c57bd5bca17ab198053", size = 263960 },
+    { url = "https://files.pythonhosted.org/packages/e7/d6/acd466c950688ed8964ade5f9c5f2c035a52b44f18f19a6d79d3de48a255/zope.interface-7.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99c14f0727c978639139e6cad7a60e82b7720922678d75aacb90cf4ef74a068c", size = 259004 },
+    { url = "https://files.pythonhosted.org/packages/71/31/44b746ed39134fa9c28262dc8ff9821c6b6f4df5a9edc1e599219d16cb79/zope.interface-7.1.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b1eed7670d564f1025d7cda89f99f216c30210e42e95de466135be0b4a499d9", size = 264463 },
+    { url = "https://files.pythonhosted.org/packages/5a/e1/30fb5f7e587e14a57c8f41413cb76eecbcfd878ef105eb908d2d2e648b73/zope.interface-7.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:3defc925c4b22ac1272d544a49c6ba04c3eefcce3200319ee1be03d9270306dd", size = 212236 },
 ]