Browse Source

add new binproviders and binaries args to install and version, bump pydantic-pkgr version

Nick Sweeting 1 year ago
parent
commit
6e7071bd19

+ 4 - 20
archivebox/abx/archivebox/base_binary.py

@@ -1,15 +1,14 @@
 __package__ = "abx.archivebox"
 
 import os
-from typing import Dict, List, Optional
+from typing import Optional, cast
 from typing_extensions import Self
 
-from pydantic import Field, InstanceOf, validate_call
+from pydantic import validate_call
 from pydantic_pkgr import (
     Binary,
     BinProvider,
     BinProviderName,
-    ProviderLookupDict,
     AptProvider,
     BrewProvider,
     EnvProvider,
@@ -25,18 +24,6 @@ from .base_hook import BaseHook, HookType
 class BaseBinProvider(BaseHook, BinProvider):
     hook_type: HookType = "BINPROVIDER"
 
-    # def on_get_abspath(self, bin_name: BinName, **context) -> Optional[HostBinPath]:
-    #     Class = super()
-    #     get_abspath_func = lambda: Class.on_get_abspath(bin_name, **context)
-    #     # return cache.get_or_set(f'bin:abspath:{bin_name}', get_abspath_func)
-    #     return get_abspath_func()
-
-    # def on_get_version(self, bin_name: BinName, abspath: Optional[HostBinPath]=None, **context) -> SemVer | None:
-    #     Class = super()
-    #     get_version_func = lambda: Class.on_get_version(bin_name, abspath, **context)
-    #     # return cache.get_or_set(f'bin:version:{bin_name}:{abspath}', get_version_func)
-    #     return get_version_func()
-
     
     # TODO: add install/load/load_or_install methods as abx.hookimpl methods
     
@@ -52,9 +39,6 @@ class BaseBinProvider(BaseHook, BinProvider):
 class BaseBinary(BaseHook, Binary):
     hook_type: HookType = "BINARY"
 
-    binproviders_supported: List[InstanceOf[BinProvider]] = Field(default_factory=list, alias="binproviders")
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = Field(default_factory=dict, alias="overrides")
-
     @staticmethod
     def symlink_to_lib(binary, bin_dir=None) -> None:
         bin_dir = bin_dir or CONSTANTS.LIB_BIN_DIR
@@ -82,13 +66,13 @@ class BaseBinary(BaseHook, Binary):
             # get cached binary from db
             try:
                 from machine.models import InstalledBinary
-                installed_binary = InstalledBinary.objects.get_from_db_or_cache(self)
+                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 binary
+        return cast(Self, binary)
     
     @validate_call
     def install(self, **kwargs) -> Self:

+ 1 - 1
archivebox/abx/archivebox/base_configset.py

@@ -9,7 +9,7 @@ from pydantic import model_validator, TypeAdapter
 from pydantic_settings import BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource
 from pydantic_settings.sources import TomlConfigSettingsSource
 
-from pydantic_pkgr.base_types import func_takes_args_or_kwargs
+from pydantic_pkgr import func_takes_args_or_kwargs
 
 import abx
 

+ 21 - 5
archivebox/cli/archivebox_install.py

@@ -22,17 +22,33 @@ def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional
         add_help=True,
         formatter_class=SmartFormatter,
     )
-    # parser.add_argument(
-    #     '--force', # '-f',
-    #     action='store_true',
-    #     help='Overwrite any existing packages that conflict with the ones ArchiveBox is trying to install',
-    # )
+    parser.add_argument(
+        '--binproviders', '-p',
+        type=str,
+        help='Select binproviders to use DEFAULT=env,apt,brew,sys_pip,venv_pip,lib_pip,pipx,sys_npm,lib_npm,puppeteer,playwright (all)',
+        default=None,
+    )
+    parser.add_argument(
+        '--binaries', '-b',
+        type=str,
+        help='Select binaries to install DEFAULT=curl,wget,git,yt-dlp,chrome,single-file,readability-extractor,postlight-parser,... (all)',
+        default=None,
+    )
+    parser.add_argument(
+        '--dry-run', '-d',
+        action='store_true',
+        help='Show what would be installed without actually installing anything',
+        default=False,
+    )
     command = parser.parse_args(args or ())   # noqa
     reject_stdin(__command__, stdin)
 
     install(
         # force=command.force,
         out_dir=Path(pwd) if pwd else DATA_DIR,
+        binaries=command.binaries.split(',') if command.binaries else None,
+        binproviders=command.binproviders.split(',') if command.binproviders else None,
+        dry_run=command.dry_run,
     )
     
 

+ 14 - 0
archivebox/cli/archivebox_version.py

@@ -27,6 +27,18 @@ def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional
         action='store_true',
         help='Only print ArchiveBox version number and nothing else.',
     )
+    parser.add_argument(
+        '--binproviders', '-p',
+        type=str,
+        help='Select binproviders to detect DEFAULT=env,apt,brew,sys_pip,venv_pip,lib_pip,pipx,sys_npm,lib_npm,puppeteer,playwright (all)',
+        default=None,
+    )
+    parser.add_argument(
+        '--binaries', '-b',
+        type=str,
+        help='Select binaries to detect DEFAULT=curl,wget,git,yt-dlp,chrome,single-file,readability-extractor,postlight-parser,... (all)',
+        default=None,
+    )
     command = parser.parse_args(args or ())
     reject_stdin(__command__, stdin)
     
@@ -40,6 +52,8 @@ def main(args: Optional[List[str]]=None, stdin: Optional[IO]=None, pwd: Optional
     version(
         quiet=command.quiet,
         out_dir=Path(pwd) if pwd else DATA_DIR,
+        binproviders=command.binproviders.split(',') if command.binproviders else None,
+        binaries=command.binaries.split(',') if command.binaries else None,
     )
 
 

+ 3 - 3
archivebox/config/views.py

@@ -111,9 +111,9 @@ def binaries_list_view(request: HttpRequest, **kwargs) -> TableContext:
                     or config_value.lower().endswith(binary.name.lower())
                     # or binary.name.lower().replace('-', '').replace('_', '') in str(config_value).lower()
             )))
-            # if not binary.provider_overrides:
+            # if not binary.overrides:
                 # import ipdb; ipdb.set_trace()
-            # rows['Overrides'].append(str(obj_to_yaml(binary.provider_overrides) or str(binary.provider_overrides))[:200])
+            # rows['Overrides'].append(str(obj_to_yaml(binary.overrides) or str(binary.overrides))[:200])
             # rows['Description'].append(binary.description)
 
     return TableContext(
@@ -153,7 +153,7 @@ def binary_detail_view(request: HttpRequest, key: str, **kwargs) -> ItemContext:
                     'binprovider': binary.loaded_binprovider,
                     'abspath': binary.loaded_abspath,
                     'version': binary.loaded_version,
-                    'overrides': obj_to_yaml(binary.provider_overrides),
+                    'overrides': obj_to_yaml(binary.overrides),
                     'providers': obj_to_yaml(binary.binproviders_supported),
                 },
                 "help_texts": {

+ 1 - 1
archivebox/machine/models.py

@@ -356,7 +356,7 @@ class InstalledBinary(ABIDModel, ModelWithHealthStats):
             'sha256': self.sha256,
             'loaded_binprovider': self.BINPROVIDER,
             'binproviders_supported': self.BINARY.binproviders_supported,
-            'provider_overrides': self.BINARY.provider_overrides,
+            'overrides': self.BINARY.overrides,
         })
 
     def load_fresh(self) -> BaseBinary:

+ 89 - 23
archivebox/main.py

@@ -179,7 +179,10 @@ def help(out_dir: Path=DATA_DIR) -> None:
 
 @enforce_types
 def version(quiet: bool=False,
-            out_dir: Path=DATA_DIR) -> None:
+            out_dir: Path=DATA_DIR,
+            binproviders: Optional[List[str]]=None,
+            binaries: Optional[List[str]]=None,
+            ) -> None:
     """Print the ArchiveBox version and dependency information"""
     
     print(VERSION)
@@ -244,6 +247,14 @@ def version(quiet: bool=False,
         if binary.name == 'archivebox':
             continue
         
+        # skip if the binary is not in the requested list of binaries
+        if binaries and binary.name not in binaries:
+            continue
+        
+        # skip if the binary is not supported by any of the requested binproviders
+        if binproviders and binary.binproviders_supported and not any(provider.name in binproviders for provider in binary.binproviders_supported):
+            continue
+        
         err = None
         try:
             loaded_bin = binary.load()
@@ -266,6 +277,9 @@ def version(quiet: bool=False,
     for name, binprovider in reversed(list(settings.BINPROVIDERS.items())):
         err = None
         
+        if binproviders and binprovider.name not in binproviders:
+            continue
+        
         # TODO: implement a BinProvider.BINARY() method that gets the loaded binary for a binprovider's INSTALLER_BIN
         loaded_bin = binprovider.INSTALLER_BINARY or BaseBinary(name=binprovider.INSTALLER_BIN, binproviders=[env, apt, brew])
         
@@ -278,25 +292,28 @@ def version(quiet: bool=False,
         PATH = str(binprovider.PATH).replace(str(DATA_DIR), '[light_slate_blue].[/light_slate_blue]').replace(str(Path('~').expanduser()), '~')
         ownership_summary = f'UID=[blue]{str(binprovider.EUID).ljust(4)}[/blue]'
         provider_summary = f'[dark_sea_green3]{str(abspath).ljust(52)}[/dark_sea_green3]' if abspath else f'[grey23]{"not available".ljust(52)}[/grey23]'
-        prnt('', '[green]√[/green]' if binprovider.is_valid else '[red]X[/red]', '', binprovider.name.ljust(11), provider_summary, ownership_summary, f'PATH={PATH}', overflow='ellipsis', soft_wrap=True)
-
-    prnt()
-    prnt('[deep_sky_blue3][i] Source-code locations:[/deep_sky_blue3]')
-    for name, path in CONSTANTS.CODE_LOCATIONS.items():
-        prnt(printable_folder_status(name, path), overflow='ignore', crop=False)
+        prnt('', '[green]√[/green]' if binprovider.is_valid else '[grey53]-[/grey53]', '', binprovider.name.ljust(11), provider_summary, ownership_summary, f'PATH={PATH}', overflow='ellipsis', soft_wrap=True)
 
-    prnt()
-    if os.access(CONSTANTS.ARCHIVE_DIR, os.R_OK) or os.access(CONSTANTS.CONFIG_FILE, os.R_OK):
-        prnt('[bright_yellow][i] Data locations:[/bright_yellow]')
-        for name, path in CONSTANTS.DATA_LOCATIONS.items():
-            prnt(printable_folder_status(name, path), overflow='ignore', crop=False)
-    
-        from archivebox.misc.checks import check_data_dir_permissions
+    if not (binaries or binproviders):
+        # dont show source code / data dir info if we just want to get version info for a binary or binprovider
         
-        check_data_dir_permissions()
-    else:
         prnt()
-        prnt('[red][i] Data locations:[/red] (not in a data directory)')
+        prnt('[deep_sky_blue3][i] Code locations:[/deep_sky_blue3]')
+        for name, path in CONSTANTS.CODE_LOCATIONS.items():
+            prnt(printable_folder_status(name, path), overflow='ignore', crop=False)
+
+        prnt()
+        if os.access(CONSTANTS.ARCHIVE_DIR, os.R_OK) or os.access(CONSTANTS.CONFIG_FILE, os.R_OK):
+            prnt('[bright_yellow][i] Data locations:[/bright_yellow]')
+            for name, path in CONSTANTS.DATA_LOCATIONS.items():
+                prnt(printable_folder_status(name, path), overflow='ignore', crop=False)
+        
+            from archivebox.misc.checks import check_data_dir_permissions
+            
+            check_data_dir_permissions()
+        else:
+            prnt()
+            prnt('[red][i] Data locations:[/red] (not in a data directory)')
         
     prnt()
     
@@ -986,7 +1003,7 @@ def list_folders(links: List[Link],
         raise ValueError('Status not recognized.')
 
 @enforce_types
-def install(out_dir: Path=DATA_DIR) -> None:
+def install(out_dir: Path=DATA_DIR, binproviders: Optional[List[str]]=None, binaries: Optional[List[str]]=None, dry_run: bool=False) -> None:
     """Automatically install all ArchiveBox dependencies and extras"""
     
     # if running as root:
@@ -1021,9 +1038,15 @@ def install(out_dir: Path=DATA_DIR) -> None:
         print()
     
     
-    package_manager_names = ', '.join(f'[yellow]{binprovider.name}[/yellow]' for binprovider in reversed(list(settings.BINPROVIDERS.values())))
+    package_manager_names = ', '.join(
+        f'[yellow]{binprovider.name}[/yellow]'
+        for binprovider in reversed(list(settings.BINPROVIDERS.values()))
+        if not binproviders or (binproviders and binprovider.name in binproviders)
+    )
     print(f'[+] Setting up package managers {package_manager_names}...')
     for binprovider in reversed(list(settings.BINPROVIDERS.values())):
+        if binproviders and binprovider.name not in binproviders:
+            continue
         try:
             binprovider.setup()
         except Exception:
@@ -1035,12 +1058,46 @@ def install(out_dir: Path=DATA_DIR) -> None:
     print()
     
     for binary in reversed(list(settings.BINARIES.values())):
-        providers = ' [grey53]or[/grey53] '.join(provider.name for provider in binary.binproviders_supported)
+        if binary.name in ('archivebox', 'django', 'sqlite', 'python', 'pipx'):
+            # obviously must already be installed if we are running
+            continue
+        
+        if binaries and binary.name not in binaries:
+            continue
+        
+        providers = ' [grey53]or[/grey53] '.join(
+            provider.name for provider in binary.binproviders_supported
+            if not binproviders or (binproviders and provider.name in binproviders)
+        )
+        if not providers:
+            continue
         print(f'[+] Detecting / Installing [yellow]{binary.name.ljust(22)}[/yellow] using [red]{providers}[/red]...')
         try:
             with SudoPermission(uid=0, fallback=True):
-                # print(binary.load_or_install(fresh=True).model_dump(exclude={'provider_overrides', 'bin_dir', 'hook_type'}))
-                binary.load_or_install(fresh=True).model_dump(exclude={'provider_overrides', 'bin_dir', 'hook_type'})
+                # print(binary.load_or_install(fresh=True).model_dump(exclude={'overrides', 'bin_dir', 'hook_type'}))
+                if binproviders:
+                    providers_supported_by_binary = [provider.name for provider in binary.binproviders_supported]
+                    for binprovider_name in binproviders:
+
+                        if binprovider_name not in providers_supported_by_binary:
+                            continue
+
+                        if dry_run:
+                            # always show install commands when doing a dry run
+                            sys.stderr.write("\033[2;49;90m")  # grey53
+                            result = binary.install(binproviders=[binprovider_name], dry_run=dry_run).model_dump(exclude={'overrides', 'bin_dir', 'hook_type'})
+                            sys.stderr.write("\033[00m\n")     # reset
+                        else:
+                            result = binary.load_or_install(binproviders=[binprovider_name], fresh=True, dry_run=dry_run).model_dump(exclude={'overrides', 'bin_dir', 'hook_type'})
+                        if result and result['loaded_version']:
+                            break
+                else:
+                    if dry_run:
+                        sys.stderr.write("\033[2;49;90m")  # grey53
+                        binary.install(dry_run=dry_run).model_dump(exclude={'overrides', 'bin_dir', 'hook_type'})
+                        sys.stderr.write("\033[00m\n")  # reset
+                    else:
+                        binary.load_or_install(fresh=True, dry_run=dry_run).model_dump(exclude={'overrides', 'bin_dir', 'hook_type'})
             if IS_ROOT:
                 with SudoPermission(uid=0):
                     if ARCHIVEBOX_USER == 0:
@@ -1049,6 +1106,9 @@ def install(out_dir: Path=DATA_DIR) -> None:
                         os.system(f'chown -R {ARCHIVEBOX_USER} "{CONSTANTS.LIB_DIR.resolve()}"')
         except Exception as e:
             print(f'[red]:cross_mark: Failed to install {binary.name} as user {ARCHIVEBOX_USER}: {e}[/red]')
+            if binaries and len(binaries) == 1:
+                # if we are only installing a single binary, raise the exception so the user can see what went wrong
+                raise
                 
 
     from django.contrib.auth import get_user_model
@@ -1063,7 +1123,13 @@ def install(out_dir: Path=DATA_DIR) -> None:
     
     from plugins_pkg.pip.apps import ARCHIVEBOX_BINARY
     
-    proc = run_shell([ARCHIVEBOX_BINARY.load().abspath, 'version'], capture_output=False, cwd=out_dir)
+    extra_args = []
+    if binproviders:
+        extra_args.append(f'--binproviders={",".join(binproviders)}')
+    if binaries:
+        extra_args.append(f'--binaries={",".join(binaries)}')
+    
+    proc = run_shell([ARCHIVEBOX_BINARY.load().abspath, 'version', *extra_args], capture_output=False, cwd=out_dir)
     raise SystemExit(proc.returncode)
 
 

+ 7 - 7
archivebox/plugins_auth/ldap/apps.py

@@ -3,11 +3,11 @@ __package__ = 'archivebox.plugins_auth.ldap'
 
 import inspect
 
-from typing import List, Dict
+from typing import List
 from pathlib import Path
 from pydantic import InstanceOf
 
-from pydantic_pkgr import BinProviderName, ProviderLookupDict, SemVer
+from pydantic_pkgr import BinaryOverrides, SemVer
 
 from abx.archivebox.base_plugin import BasePlugin
 from abx.archivebox.base_hook import BaseHook
@@ -43,26 +43,26 @@ class LdapBinary(BaseBinary):
     description: str = 'LDAP Authentication'
     binproviders_supported: List[InstanceOf[BaseBinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, LIB_PIP_BINPROVIDER, apt]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         LIB_PIP_BINPROVIDER.name: {
             "abspath": lambda: get_LDAP_LIB_path(LIB_SITE_PACKAGES),
             "version": lambda: get_LDAP_LIB_version(),
-            "packages": lambda: ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
+            "packages": ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
         },
         VENV_PIP_BINPROVIDER.name: {
             "abspath": lambda: get_LDAP_LIB_path(VENV_SITE_PACKAGES),
             "version": lambda: get_LDAP_LIB_version(),
-            "packages": lambda: ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
+            "packages": ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
         },
         SYS_PIP_BINPROVIDER.name: {
             "abspath": lambda: get_LDAP_LIB_path((*USER_SITE_PACKAGES, *SYS_SITE_PACKAGES)),
             "version": lambda: get_LDAP_LIB_version(),
-            "packages": lambda: ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
+            "packages": ['python-ldap>=3.4.3', 'django-auth-ldap>=4.1.0'],
         },
         apt.name: {
             "abspath": lambda: get_LDAP_LIB_path(),
             "version": lambda: get_LDAP_LIB_version(),
-            "packages": lambda: ['libssl-dev', 'libldap2-dev', 'libsasl2-dev', 'python3-ldap', 'python3-msgpack', 'python3-mutagen'],
+            "packages": ['libssl-dev', 'libldap2-dev', 'libsasl2-dev', 'python3-ldap', 'python3-msgpack', 'python3-mutagen'],
         },
     }
 

+ 4 - 4
archivebox/plugins_extractor/chrome/apps.py

@@ -13,7 +13,7 @@ from pydantic_pkgr import (
     BinProvider,
     BinName,
     BinProviderName,
-    ProviderLookupDict,
+    BinaryOverrides,
     bin_abspath,
 )
 
@@ -204,15 +204,15 @@ class ChromeBinary(BaseBinary):
     name: BinName = CHROME_CONFIG.CHROME_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [PUPPETEER_BINPROVIDER, env, PLAYWRIGHT_BINPROVIDER]
     
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         env.name: {
             'abspath': lambda: autodetect_system_chrome_install(PATH=env.PATH),  # /usr/bin/google-chrome-stable
         },
         PUPPETEER_BINPROVIDER.name: {
-            'packages': lambda: ['chrome@stable'],              # npx @puppeteer/browsers install chrome@stable
+            'packages': ['chrome@stable'],              # npx @puppeteer/browsers install chrome@stable
         },
         PLAYWRIGHT_BINPROVIDER.name: {
-            'packages': lambda: ['chromium'],                   # playwright install chromium
+            'packages': ['chromium'],                   # playwright install chromium
         },
     }
 

+ 6 - 6
archivebox/plugins_extractor/mercury/apps.py

@@ -1,10 +1,10 @@
 __package__ = 'plugins_extractor.mercury'
 
-from typing import List, Optional, Dict
+from typing import List, Optional
 from pathlib import Path
 
 from pydantic import InstanceOf, Field
-from pydantic_pkgr import BinProvider, BinName, BinProviderName, ProviderLookupDict, bin_abspath
+from pydantic_pkgr import BinProvider, BinName, BinaryOverrides, bin_abspath
 
 from abx.archivebox.base_plugin import BasePlugin, BaseHook
 from abx.archivebox.base_configset import BaseConfigSet
@@ -38,13 +38,13 @@ class MercuryBinary(BaseBinary):
     name: BinName = MERCURY_CONFIG.MERCURY_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_NPM_BINPROVIDER, SYS_NPM_BINPROVIDER, env]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         LIB_NPM_BINPROVIDER.name: {
-            'packages': lambda: ['@postlight/parser@^2.2.3'],
+            'packages': ['@postlight/parser@^2.2.3'],
         },
         SYS_NPM_BINPROVIDER.name: {
-            'packages': lambda: ['@postlight/parser@^2.2.3'],
-            'install': lambda: False,                          # never try to install things into global prefix
+            'packages': ['@postlight/parser@^2.2.3'],
+            'install': lambda: None,                          # never try to install things into global prefix
         },
         env.name: {
             'version': lambda: '999.999.999' if bin_abspath('postlight-parser', PATH=env.PATH) else None,

+ 6 - 19
archivebox/plugins_extractor/readability/apps.py

@@ -1,12 +1,12 @@
 __package__ = 'archivebox.plugins_extractor.readability'
 
 from pathlib import Path
-from typing import List, Dict, Optional
+from typing import List
 # from typing_extensions import Self
 
 # Depends on other PyPI/vendor packages:
-from pydantic import InstanceOf, Field, validate_call
-from pydantic_pkgr import BinProvider, BinProviderName, ProviderLookupDict, BinName, ShallowBinary
+from pydantic import InstanceOf, Field
+from pydantic_pkgr import BinProvider, BinaryOverrides, BinName
 
 # Depends on other Django apps:
 from abx.archivebox.base_plugin import BasePlugin
@@ -39,23 +39,10 @@ class ReadabilityBinary(BaseBinary):
     name: BinName = READABILITY_CONFIG.READABILITY_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_NPM_BINPROVIDER, SYS_NPM_BINPROVIDER, env]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
-        LIB_NPM_BINPROVIDER.name: {"packages": lambda: [READABILITY_PACKAGE_NAME]},
-        SYS_NPM_BINPROVIDER.name: {"packages": lambda: []},    # prevent modifying system global npm packages
+    overrides: BinaryOverrides = {
+        LIB_NPM_BINPROVIDER.name: {"packages": [READABILITY_PACKAGE_NAME]},
+        SYS_NPM_BINPROVIDER.name: {"packages": [READABILITY_PACKAGE_NAME], "install": lambda: None},    # prevent modifying system global npm packages
     }
-    
-    @validate_call
-    def install(self, binprovider_name: Optional[BinProviderName]=None, **kwargs) -> ShallowBinary:
-        # force install to only use lib/npm provider, we never want to modify global NPM packages
-        return BaseBinary.install(self, binprovider_name=binprovider_name or LIB_NPM_BINPROVIDER.name, **kwargs)
-    
-    @validate_call
-    def load_or_install(self, binprovider_name: Optional[BinProviderName] = None, fresh=False, **kwargs) -> ShallowBinary:
-        try:
-            return self.load(fresh=fresh)
-        except Exception:
-            # force install to only use lib/npm provider, we never want to modify global NPM packages
-            return BaseBinary.install(self, binprovider_name=binprovider_name or LIB_NPM_BINPROVIDER.name, **kwargs)
 
 
 

+ 6 - 19
archivebox/plugins_extractor/singlefile/apps.py

@@ -1,12 +1,12 @@
 __package__ = 'archivebox.plugins_extractor.singlefile'
 
 from pathlib import Path
-from typing import List, Dict, Optional
+from typing import List, Optional
 # from typing_extensions import Self
 
 # Depends on other PyPI/vendor packages:
 from pydantic import InstanceOf, Field
-from pydantic_pkgr import BinProvider, BinProviderName, ProviderLookupDict, BinName, bin_abspath, ShallowBinary
+from pydantic_pkgr import BinProvider, BinaryOverrides, BinName, bin_abspath
 
 # Depends on other Django apps:
 from abx.archivebox.base_plugin import BasePlugin
@@ -45,22 +45,21 @@ class SinglefileBinary(BaseBinary):
     name: BinName = SINGLEFILE_CONFIG.SINGLEFILE_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_NPM_BINPROVIDER, SYS_NPM_BINPROVIDER, env]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         LIB_NPM_BINPROVIDER.name: {
             "abspath": lambda:
                 bin_abspath(SINGLEFILE_CONFIG.SINGLEFILE_BINARY, PATH=LIB_NPM_BINPROVIDER.PATH)
                 or bin_abspath("single-file", PATH=LIB_NPM_BINPROVIDER.PATH)
                 or bin_abspath("single-file-node.js", PATH=LIB_NPM_BINPROVIDER.PATH),
-            "packages": lambda:
-                [f"single-file-cli@>={SINGLEFILE_MIN_VERSION} <{SINGLEFILE_MAX_VERSION}"],
+            "packages": [f"single-file-cli@>={SINGLEFILE_MIN_VERSION} <{SINGLEFILE_MAX_VERSION}"],
         },
         SYS_NPM_BINPROVIDER.name: {
             "abspath": lambda:
                 bin_abspath(SINGLEFILE_CONFIG.SINGLEFILE_BINARY, PATH=SYS_NPM_BINPROVIDER.PATH)
                 or bin_abspath("single-file", PATH=SYS_NPM_BINPROVIDER.PATH)
                 or bin_abspath("single-file-node.js", PATH=SYS_NPM_BINPROVIDER.PATH),
-            "packages": lambda:
-                [],    # prevent modifying system global npm packages
+            "packages": [f"single-file-cli@>={SINGLEFILE_MIN_VERSION} <{SINGLEFILE_MAX_VERSION}"],
+            "install": lambda: None,
         },
         env.name: {
             'abspath': lambda:
@@ -69,18 +68,6 @@ class SinglefileBinary(BaseBinary):
                 or bin_abspath('single-file-node.js', PATH=env.PATH),
         },
     }
-    
-    def install(self, binprovider_name: Optional[BinProviderName]=None, **kwargs) -> ShallowBinary:
-        # force install to only use lib/npm provider, we never want to modify global NPM packages
-        return BaseBinary.install(self, binprovider_name=binprovider_name or LIB_NPM_BINPROVIDER.name, **kwargs)
-    
-    def load_or_install(self, binprovider_name: Optional[BinProviderName]=None, fresh=False, **kwargs) -> ShallowBinary:
-        try:
-            return self.load(fresh=fresh)
-        except Exception:
-            # force install to only use lib/npm provider, we never want to modify global NPM packages
-            return BaseBinary.install(self, binprovider_name=binprovider_name or LIB_NPM_BINPROVIDER.name, **kwargs)
-
 
 
 SINGLEFILE_BINARY = SinglefileBinary()

+ 4 - 4
archivebox/plugins_extractor/ytdlp/apps.py

@@ -1,10 +1,10 @@
 import sys
-from typing import List, Dict
+from typing import List
 from subprocess import run, PIPE
 
 from rich import print
 from pydantic import InstanceOf, Field, model_validator, AliasChoices
-from pydantic_pkgr import BinProvider, BinName, BinProviderName, ProviderLookupDict
+from pydantic_pkgr import BinProvider, BinName, BinaryOverrides
 
 from abx.archivebox.base_plugin import BasePlugin
 from abx.archivebox.base_configset import BaseConfigSet
@@ -54,10 +54,10 @@ class FfmpegBinary(BaseBinary):
     name: BinName = 'ffmpeg'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         'env': {
             # 'abspath': lambda: shutil.which('ffmpeg', PATH=env.PATH),
-            # 'version': lambda: run(['ffmpeg', '-version'], stdout=PIPE, stderr=PIPE, text=True).stdout,
+            'version': lambda: run(['ffmpeg', '-version'], stdout=PIPE, stderr=PIPE, text=True).stdout,
         },
         'apt': {
             # 'abspath': lambda: shutil.which('ffmpeg', PATH=apt.PATH),

+ 6 - 6
archivebox/plugins_pkg/npm/apps.py

@@ -1,11 +1,11 @@
 __package__ = 'archivebox.plugins_pkg.npm'
 
 from pathlib import Path
-from typing import List, Optional, Dict
+from typing import List, Optional
 
 from pydantic import InstanceOf, model_validator
 
-from pydantic_pkgr import BinProvider, NpmProvider, BinName, PATHStr, BinProviderName, ProviderLookupDict
+from pydantic_pkgr import BinProvider, NpmProvider, BinName, PATHStr, BinProviderName, BinaryOverrides
 
 from archivebox.config import DATA_DIR, CONSTANTS
 
@@ -60,8 +60,8 @@ class NodeBinary(BaseBinary):
     name: BinName = 'node'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
     
-    overrides: Dict[BinProviderName, ProviderLookupDict] = {
-        apt.name: {'packages': lambda c: ['nodejs']},
+    overrides: BinaryOverrides = {
+        apt.name: {'packages': ['nodejs']},
     }
 
 
@@ -72,7 +72,7 @@ class NpmBinary(BaseBinary):
     name: BinName = 'npm'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
 
-    overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         apt.name: {'install': lambda: None},   # already installed when nodejs is installed
         brew.name: {'install': lambda: None},  # already installed when nodejs is installed
     }
@@ -84,7 +84,7 @@ class NpxBinary(BaseBinary):
     name: BinName = 'npx'
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
     
-    overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         apt.name: {'install': lambda: None},   # already installed when nodejs is installed
         brew.name: {'install': lambda: None},  # already installed when nodejs is installed
     }

+ 30 - 30
archivebox/plugins_pkg/pip/apps.py

@@ -4,14 +4,14 @@ import os
 import sys
 import site
 from pathlib import Path
-from typing import List, Dict, Optional
+from typing import List, Optional
 from pydantic import InstanceOf, Field, model_validator, validate_call
 
 
 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, PipProvider, BinName, BinProviderName, ProviderLookupDict, SemVer
+from pydantic_pkgr import BinProvider, PipProvider, BinName, BinProviderName, BinaryOverrides, SemVer
 
 from archivebox.config import CONSTANTS, VERSION
 
@@ -105,18 +105,18 @@ class ArchiveboxBinary(BaseBinary):
     name: BinName = 'archivebox'
 
     binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
-        VENV_PIP_BINPROVIDER.name:  {'packages': lambda: [], 'version': lambda: VERSION},
-        SYS_PIP_BINPROVIDER.name:   {'packages': lambda: [], 'version': lambda: VERSION},
-        apt.name:                   {'packages': lambda: [], 'version': lambda: VERSION},
-        brew.name:                  {'packages': lambda: [], 'version': lambda: VERSION},
+    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},
     }
     
-    @validate_call
+    # @validate_call
     def install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
     
-    @validate_call
+    # @validate_call
     def load_or_install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
 
@@ -127,18 +127,18 @@ class PythonBinary(BaseBinary):
     name: BinName = 'python'
 
     binproviders_supported: List[InstanceOf[BinProvider]] = [VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         SYS_PIP_BINPROVIDER.name: {
-            'abspath': lambda: sys.executable,
-            'version': lambda: '{}.{}.{}'.format(*sys.version_info[:3]),
+            'abspath': sys.executable,
+            'version': '{}.{}.{}'.format(*sys.version_info[:3]),
         },
     }
     
-    @validate_call
+    # @validate_call
     def install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
     
-    @validate_call
+    # @validate_call
     def load_or_install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
 
@@ -152,14 +152,14 @@ LOADED_SQLITE_FROM_VENV = str(LOADED_SQLITE_PATH.absolute().resolve()).startswit
 class SqliteBinary(BaseBinary):
     name: BinName = 'sqlite'
     binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         VENV_PIP_BINPROVIDER.name: {
-            "abspath": lambda: LOADED_SQLITE_PATH if LOADED_SQLITE_FROM_VENV else None,
-            "version": lambda: LOADED_SQLITE_VERSION if LOADED_SQLITE_FROM_VENV else None,
+            "abspath": LOADED_SQLITE_PATH if LOADED_SQLITE_FROM_VENV else None,
+            "version": LOADED_SQLITE_VERSION if LOADED_SQLITE_FROM_VENV else None,
         },
         SYS_PIP_BINPROVIDER.name: {
-            "abspath": lambda: LOADED_SQLITE_PATH if not LOADED_SQLITE_FROM_VENV else None,
-            "version": lambda: LOADED_SQLITE_VERSION if not LOADED_SQLITE_FROM_VENV else None,
+            "abspath": LOADED_SQLITE_PATH if not LOADED_SQLITE_FROM_VENV else None,
+            "version": LOADED_SQLITE_VERSION if not LOADED_SQLITE_FROM_VENV else None,
         },
     }
     
@@ -177,11 +177,11 @@ class SqliteBinary(BaseBinary):
             ])
         return self
     
-    @validate_call
+    # @validate_call
     def install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
     
-    @validate_call
+    # @validate_call
     def load_or_install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
 
@@ -196,22 +196,22 @@ class DjangoBinary(BaseBinary):
     name: BinName = 'django'
 
     binproviders_supported: List[InstanceOf[BaseBinProvider]] = Field(default=[VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER])
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
+    overrides: BinaryOverrides = {
         VENV_PIP_BINPROVIDER.name: {
-            "abspath": lambda: LOADED_DJANGO_PATH if LOADED_DJANGO_FROM_VENV else None,
-            "version": lambda: LOADED_DJANGO_VERSION if LOADED_DJANGO_FROM_VENV else None,
+            "abspath": LOADED_DJANGO_PATH if LOADED_DJANGO_FROM_VENV else None,
+            "version": LOADED_DJANGO_VERSION if LOADED_DJANGO_FROM_VENV else None,
         },
         SYS_PIP_BINPROVIDER.name: {
-            "abspath": lambda: LOADED_DJANGO_PATH if not LOADED_DJANGO_FROM_VENV else None,
-            "version": lambda: LOADED_DJANGO_VERSION if not LOADED_DJANGO_FROM_VENV else None,
+            "abspath": LOADED_DJANGO_PATH if not LOADED_DJANGO_FROM_VENV else None,
+            "version": LOADED_DJANGO_VERSION if not LOADED_DJANGO_FROM_VENV else None,
         },
     }
     
-    @validate_call
+    # @validate_call
     def install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
     
-    @validate_call
+    # @validate_call
     def load_or_install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
 
@@ -221,11 +221,11 @@ class PipBinary(BaseBinary):
     name: BinName = "pip"
     binproviders_supported: List[InstanceOf[BinProvider]] = [LIB_PIP_BINPROVIDER, VENV_PIP_BINPROVIDER, SYS_PIP_BINPROVIDER, apt, brew, env]
 
-    @validate_call
+    # @validate_call
     def install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
     
-    @validate_call
+    # @validate_call
     def load_or_install(self, **kwargs):
         return self.load()                  # obviously it's already installed if we are running this ;)
 

+ 18 - 10
archivebox/plugins_pkg/playwright/apps.py

@@ -11,7 +11,7 @@ from pydantic_pkgr import (
     BinName,
     BinProvider,
     BinProviderName,
-    ProviderLookupDict,
+    BinProviderOverrides,
     InstallArgs,
     PATHStr,
     HostBinPath,
@@ -66,15 +66,15 @@ class PlaywrightBinProvider(BaseBinProvider):
 
     PATH: PATHStr = f"{CONSTANTS.LIB_BIN_DIR}:{DEFAULT_ENV_PATH}"
 
-    playwright_browsers_dir: Optional[Path] = (
+    playwright_browsers_dir: Path = (
         Path("~/Library/Caches/ms-playwright").expanduser()      # macos playwright cache dir
         if OPERATING_SYSTEM == "darwin" else
         Path("~/.cache/ms-playwright").expanduser()              # linux playwright cache dir
     )
     playwright_install_args: List[str] = ["install"]              # --with-deps
 
-    packages_handler: ProviderLookupDict = Field(default={
-        "chrome": lambda: ["chromium"],
+    packages_handler: BinProviderOverrides = Field(default={
+        "chrome": ["chromium"],
     }, exclude=True)
 
     _browser_abspaths: ClassVar[Dict[str, HostBinPath]] = {}
@@ -104,9 +104,17 @@ class PlaywrightBinProvider(BaseBinProvider):
             )
 
         # ~/Library/caches/ms-playwright/chromium-1097/chrome-linux/chromium
-        return sorted(self.playwright_browsers_dir.glob(f"{browser_name}-*/*-linux/*"))
-
-    def on_get_abspath(self, bin_name: BinName, **context) -> Optional[HostBinPath]:
+        paths = []
+        for path in sorted(self.playwright_browsers_dir.glob(f"{browser_name}-*/*-linux/*")):
+            if 'xdg-settings' in str(path):
+                continue
+            if 'ffmpeg' in str(path):
+                continue
+            if '/chrom' in str(path) and 'chrom' in path.name.lower():
+                paths.append(path)
+        return paths
+
+    def default_abspath_handler(self, bin_name: BinName, **context) -> Optional[HostBinPath]:
         assert bin_name == "chrome", "Only chrome is supported using the @puppeteer/browsers install method currently."
 
         # already loaded, return abspath from cache
@@ -128,7 +136,7 @@ class PlaywrightBinProvider(BaseBinProvider):
 
         return None
 
-    def on_install(self, bin_name: str, packages: Optional[InstallArgs] = None, **context) -> str:
+    def default_install_handler(self, bin_name: str, packages: Optional[InstallArgs] = None, **context) -> str:
         """playwright install chrome"""
         self.setup()
         assert bin_name == "chrome", "Only chrome is supported using the playwright install method currently."
@@ -137,7 +145,7 @@ class PlaywrightBinProvider(BaseBinProvider):
             raise Exception(
                 f"{self.__class__.__name__} install method is not available on this host ({self.INSTALLER_BIN} not found in $PATH)"
             )
-        packages = packages or self.on_get_packages(bin_name)
+        packages = packages or self.get_packages(bin_name)
 
         # print(f'[*] {self.__class__.__name__}: Installing {bin_name}: {self.INSTALLER_BIN_ABSPATH} install {packages}')
 
@@ -155,7 +163,7 @@ class PlaywrightBinProvider(BaseBinProvider):
         output_lines = [
             line for line in proc.stdout.strip().split('\n')
             if '/chrom' in line
-            and 'chrom' in line.rsplit('/', 1)[-1].lower()   # make final path segment (filename) contains chrome or chromium
+            and 'chrom' in line.rsplit('/', 1)[-1].lower()   # if final path segment (filename) contains chrome or chromium
             and 'xdg-settings' not in line
             and 'ffmpeg' not in line
         ]

+ 6 - 6
archivebox/plugins_pkg/puppeteer/apps.py

@@ -11,7 +11,7 @@ from pydantic_pkgr import (
     BinProvider,
     BinName,
     BinProviderName,
-    ProviderLookupDict,
+    BinProviderOverrides,
     InstallArgs,
     PATHStr,
     HostBinPath,
@@ -65,10 +65,10 @@ class PuppeteerBinProvider(BaseBinProvider):
     
     euid: Optional[int] = ARCHIVEBOX_USER
 
-    puppeteer_browsers_dir: Optional[Path] = LIB_DIR_BROWSERS
+    puppeteer_browsers_dir: Path = LIB_DIR_BROWSERS
     puppeteer_install_args: List[str] = ["@puppeteer/browsers", "install", "--path", str(LIB_DIR_BROWSERS)]
 
-    packages_handler: ProviderLookupDict = Field(default={
+    packages_handler: BinProviderOverrides = Field(default={
         "chrome": lambda:
             ['chrome@stable'],
     }, exclude=True)
@@ -90,7 +90,7 @@ class PuppeteerBinProvider(BaseBinProvider):
         # /data/lib/browsers/chrome/linux-131.0.6730.0/chrome-linux64/chrome
         return sorted(self.puppeteer_browsers_dir.glob(f"{browser_name}/linux*/chrome*/chrome"))
 
-    def on_get_abspath(self, bin_name: BinName, **context) -> Optional[HostBinPath]:
+    def default_abspath_handler(self, bin_name: BinName, **context) -> Optional[HostBinPath]:
         assert bin_name == 'chrome', 'Only chrome is supported using the @puppeteer/browsers install method currently.'
         
         # already loaded, return abspath from cache
@@ -106,7 +106,7 @@ class PuppeteerBinProvider(BaseBinProvider):
         
         return None
 
-    def on_install(self, bin_name: str, packages: Optional[InstallArgs] = None, **context) -> str:
+    def default_install_handler(self, bin_name: str, packages: Optional[InstallArgs] = None, **context) -> str:
         """npx @puppeteer/browsers install chrome@stable"""
         self.setup()
         assert bin_name == 'chrome', 'Only chrome is supported using the @puppeteer/browsers install method currently.'
@@ -115,7 +115,7 @@ class PuppeteerBinProvider(BaseBinProvider):
             raise Exception(
                 f"{self.__class__.__name__} install method is not available on this host ({self.INSTALLER_BIN} not found in $PATH)"
             )
-        packages = packages or self.on_get_packages(bin_name)
+        packages = packages or self.get_packages(bin_name)
         assert packages, f"No packages specified for installation of {bin_name}"
 
         # print(f'[*] {self.__class__.__name__}: Installing {bin_name}: {self.INSTALLER_BIN_ABSPATH} install {packages}')

+ 5 - 5
archivebox/plugins_search/ripgrep/apps.py

@@ -3,12 +3,12 @@ __package__ = 'archivebox.plugins_search.ripgrep'
 import re
 from pathlib import Path
 from subprocess import run
-from typing import List, Dict, Iterable
+from typing import List, Iterable
 # from typing_extensions import Self
 
 # Depends on other PyPI/vendor packages:
 from pydantic import InstanceOf, Field
-from pydantic_pkgr import BinProvider, BinProviderName, ProviderLookupDict, BinName
+from pydantic_pkgr import BinProvider, BinaryOverrides, BinName
 
 # Depends on other Django apps:
 from abx.archivebox.base_plugin import BasePlugin
@@ -45,9 +45,9 @@ class RipgrepBinary(BaseBinary):
     name: BinName = RIPGREP_CONFIG.RIPGREP_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [apt, brew, env]
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
-        apt.name: {'packages': lambda: ['ripgrep']},
-        brew.name: {'packages': lambda: ['ripgrep']},
+    overrides: BinaryOverrides = {
+        apt.name: {'packages': ['ripgrep']},
+        brew.name: {'packages': ['ripgrep']},
     }
 
 RIPGREP_BINARY = RipgrepBinary()

+ 5 - 5
archivebox/plugins_search/sonic/apps.py

@@ -1,11 +1,11 @@
 __package__ = 'archivebox.plugins_search.sonic'
 
 import sys
-from typing import List, Dict, Generator, cast
+from typing import List, Generator, cast
 
 # Depends on other PyPI/vendor packages:
 from pydantic import InstanceOf, Field, model_validator
-from pydantic_pkgr import BinProvider, BinProviderName, ProviderLookupDict, BinName
+from pydantic_pkgr import BinProvider, BinaryOverrides, BinName
 
 # Depends on other Django apps:
 from abx.archivebox.base_plugin import BasePlugin
@@ -55,9 +55,9 @@ class SonicBinary(BaseBinary):
     name: BinName = SONIC_CONFIG.SONIC_BINARY
     binproviders_supported: List[InstanceOf[BinProvider]] = [brew, env]   # TODO: add cargo
 
-    provider_overrides: Dict[BinProviderName, ProviderLookupDict] = {
-        brew.name: {'packages': lambda: ['sonic']},
-        # cargo.name: {'packages': lambda: ['sonic-server']},             # TODO: add cargo
+    overrides: BinaryOverrides = {
+        brew.name: {'packages': ['sonic']},
+        # cargo.name: {'packages': ['sonic-server']},                     # TODO: add cargo
     }
     
     # TODO: add version checking over protocol? for when sonic backend is on remote server and binary is not installed locally

+ 2 - 2
archivebox/plugins_search/sqlite/apps.py

@@ -66,11 +66,11 @@ class SqliteftsConfig(BaseConfigSet):
         # Only Python >= 3.11 supports sqlite3.Connection.getlimit(),
         # so fall back to the default if the API to get the real value isn't present
         try:
-            limit_id = sqlite3.SQLITE_LIMIT_LENGTH
+            limit_id = sqlite3.SQLITE_LIMIT_LENGTH              # type: ignore[attr-defined]
             
             if self.SQLITEFTS_SEPARATE_DATABASE:
                 cursor = self.get_connection()
-                return cursor.connection.getlimit(limit_id)
+                return cursor.connection.getlimit(limit_id)     # type: ignore[attr-defined]
             else:
                 with database.temporary_connection() as cursor:  # type: ignore[attr-defined]
                     return cursor.connection.getlimit(limit_id)

+ 1 - 1
archivebox/vendor/pydantic-pkgr

@@ -1 +1 @@
-Subproject commit 9d33c8c75ebfc7ea99e29fcc8126d081a8026cda
+Subproject commit ad3c0ca457951d4d0852b46020fc6365b75e5065

+ 2 - 2
pyproject.toml

@@ -1,6 +1,6 @@
 [project]
 name = "archivebox"
-version = "0.8.5rc37"
+version = "0.8.5rc42"
 requires-python = ">=3.10"
 description = "Self-hosted internet archiving solution."
 authors = [{name = "Nick Sweeting", email = "[email protected]"}]
@@ -78,7 +78,7 @@ dependencies = [
     "django-taggit==1.3.0",
     "base32-crockford==0.3.0",
     # "pocket@git+https://github.com/tapanpandita/[email protected]",
-    "pydantic-pkgr>=0.4.24",
+    "pydantic-pkgr>=0.5.2",
     ############# Plugin Dependencies ################
     "sonic-client>=1.0.0",
     "yt-dlp>=2024.8.6",               # for: media"

+ 5 - 3
requirements.txt

@@ -119,7 +119,7 @@ executing==2.1.0
     # via stack-data
 feedparser==6.0.11
     # via archivebox (pyproject.toml)
-ftfy==6.2.3
+ftfy==6.3.0
     # via python-benedict
 h11==0.14.0
     # via httpcore
@@ -168,6 +168,8 @@ pexpect==4.9.0
     # via ipython
 phonenumbers==8.13.47
     # via python-benedict
+platformdirs==4.3.6
+    # via pydantic-pkgr
 pluggy==1.5.0
     # via archivebox (pyproject.toml)
 prompt-toolkit==3.0.48
@@ -203,7 +205,7 @@ pydantic-core==2.23.4
     # via
     #   pydantic
     #   pydantic-pkgr
-pydantic-pkgr==0.4.24
+pydantic-pkgr==0.5.2
     # via archivebox (pyproject.toml)
 pydantic-settings==2.5.2
     # via archivebox (pyproject.toml)
@@ -332,5 +334,5 @@ xmltodict==0.14.1
     # via python-benedict
 yt-dlp==2024.10.7
     # via archivebox (pyproject.toml)
-zope-interface==7.0.3
+zope-interface==7.1.0
     # via twisted

+ 75 - 56
uv.lock

@@ -41,7 +41,7 @@ wheels = [
 
 [[package]]
 name = "archivebox"
-version = "0.8.5rc36"
+version = "0.8.5rc42"
 source = { editable = "." }
 dependencies = [
     { name = "atomicwrites" },
@@ -148,7 +148,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.4.24" },
+    { name = "pydantic-pkgr", specifier = ">=0.5.2" },
     { name = "pydantic-settings", specifier = ">=2.5.2" },
     { name = "python-benedict", extras = ["io", "parse"], specifier = ">=0.33.2" },
     { name = "python-crontab", specifier = ">=3.2.0" },
@@ -965,14 +965,14 @@ wheels = [
 
 [[package]]
 name = "ftfy"
-version = "6.2.3"
+version = "6.3.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "wcwidth" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/da/a9/59f4354257e8350a25be1774021991fb3a99a2fb87d0c1f367592548aed3/ftfy-6.2.3.tar.gz", hash = "sha256:79b505988f29d577a58a9069afe75553a02a46e42de6091c0660cdc67812badc", size = 64165 }
+sdist = { url = "https://files.pythonhosted.org/packages/85/c3/63753eca4c5257ce0561cb5f8e9cd0d45d97848c73c56e33a0a764319e5b/ftfy-6.3.0.tar.gz", hash = "sha256:1c7d6418e72b25a7760feb150acf574b86924dbb2e95b32c0b3abbd1ba3d7ad6", size = 362118 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/ed/46/14d230ad057048aea7ccd2f96a80905830866d281ea90a6662a825490659/ftfy-6.2.3-py3-none-any.whl", hash = "sha256:f15761b023f3061a66207d33f0c0149ad40a8319fd16da91796363e2c049fdf8", size = 43011 },
+    { url = "https://files.pythonhosted.org/packages/76/0f/d8a8152e720cbcad890e56ee98639ff489f1992869b4cf304c3fa24d4bcc/ftfy-6.3.0-py3-none-any.whl", hash = "sha256:17aca296801f44142e3ff2c16f93fbf6a87609ebb3704a9a41dd5d4903396caf", size = 44778 },
 ]
 
 [[package]]
@@ -1170,31 +1170,37 @@ wheels = [
 
 [[package]]
 name = "libcst"
-version = "1.4.0"
+version = "1.5.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "pyyaml" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/e4/bd/ff41d7a8efc4f60a61d903c3f9823565006f44f2b8b11c99701f552b0851/libcst-1.4.0.tar.gz", hash = "sha256:449e0b16604f054fa7f27c3ffe86ea7ef6c409836fe68fe4e752a1894175db00", size = 771364 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/09/a2/00a395a95518626cfd67aeed1d3e9f39b82b5e42e025bea897e1226db41b/libcst-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:279b54568ea1f25add50ea4ba3d76d4f5835500c82f24d54daae4c5095b986aa", size = 2110691 },
-    { url = "https://files.pythonhosted.org/packages/53/4d/8353b566a9c338b46af01f3758296d5646808dd314c0b686f77384c0d323/libcst-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3401dae41fe24565387a65baee3887e31a44e3e58066b0250bc3f3ccf85b1b5a", size = 2036754 },
-    { url = "https://files.pythonhosted.org/packages/e6/c9/9cea10a2c2dcb120a793616ceac0ab9548c05edb06e4f824f6e88c86c8e8/libcst-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1989fa12d3cd79118ebd29ebe2a6976d23d509b1a4226bc3d66fcb7cb50bd5d", size = 2199222 },
-    { url = "https://files.pythonhosted.org/packages/25/5f/0df8f628122a5cd114b9edfbc673cb56070fdb295e355048a076a40d5974/libcst-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:addc6d585141a7677591868886f6bda0577529401a59d210aa8112114340e129", size = 2251349 },
-    { url = "https://files.pythonhosted.org/packages/3f/0d/2db8d0df21eab1a10c89218123cabb667d7c546dff6253bdc56480d707e0/libcst-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17d71001cb25e94cfe8c3d997095741a8c4aa7a6d234c0f972bc42818c88dfaf", size = 2335344 },
-    { url = "https://files.pythonhosted.org/packages/b2/1b/1a2b83d208ea4d91b955be2a4e6b3cec0a647e6d6aa032d3b59f1585de31/libcst-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:2d47de16d105e7dd5f4e01a428d9f4dc1e71efd74f79766daf54528ce37f23c3", size = 2029201 },
-    { url = "https://files.pythonhosted.org/packages/85/2c/6bf8e4710afe1e0d45643e3726c0a956f5965555425cd7efa31e97cc7a6b/libcst-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e6227562fc5c9c1efd15dfe90b0971ae254461b8b6b23c1b617139b6003de1c1", size = 2110723 },
-    { url = "https://files.pythonhosted.org/packages/5d/82/652e041aa6e14751a2ce41e68e281d9d5a32864ba11a363e103c429bf0e8/libcst-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3399e6c95df89921511b44d8c5bf6a75bcbc2d51f1f6429763609ba005c10f6b", size = 2036982 },
-    { url = "https://files.pythonhosted.org/packages/b8/d7/515b6187a900033467a4001bf8e2ed95f4961aa9bedf2bf39dfd68659157/libcst-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48601e3e590e2d6a7ab8c019cf3937c70511a78d778ab3333764531253acdb33", size = 2199286 },
-    { url = "https://files.pythonhosted.org/packages/50/a1/2093f74a3f8936fcdaac01f86d1c5fa8f586202afa466a92332b9a461b14/libcst-1.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f42797309bb725f0f000510d5463175ccd7155395f09b5e7723971b0007a976d", size = 2251591 },
-    { url = "https://files.pythonhosted.org/packages/0a/6c/1eb258b0eba8f337e1e9bd40574247310670c036a3913c9b650d6d9cd4de/libcst-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb4e42ea107a37bff7f9fdbee9532d39f9ea77b89caa5c5112b37057b12e0838", size = 2335434 },
-    { url = "https://files.pythonhosted.org/packages/6a/56/1c5a8385e9cc2d95d278cb8df48d11587c1c93b3b78c2edafd16b2bf11fa/libcst-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:9d0cc3c5a2a51fa7e1d579a828c0a2e46b2170024fd8b1a0691c8a52f3abb2d9", size = 2029195 },
-    { url = "https://files.pythonhosted.org/packages/2f/09/e4374c8e9bde82a6197860b67ed0b0cd07c0fbc95fff035886382165a279/libcst-1.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7ece51d935bc9bf60b528473d2e5cc67cbb88e2f8146297e40ee2c7d80be6f13", size = 2106058 },
-    { url = "https://files.pythonhosted.org/packages/61/8a/84810ea960ede8d15266cc5e135165d92aadb08956136e53926b3e037829/libcst-1.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:81653dea1cdfa4c6520a7c5ffb95fa4d220cbd242e446c7a06d42d8636bfcbba", size = 2032124 },
-    { url = "https://files.pythonhosted.org/packages/08/1d/3e2ab936e4195df82b764b02631a865b65dcf252772ddfe5265d384a883d/libcst-1.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6abce0e66bba2babfadc20530fd3688f672d565674336595b4623cd800b91ef", size = 2195173 },
-    { url = "https://files.pythonhosted.org/packages/11/38/30206bbcf31425f6fd01dae3cf23e35df790969243d39757ae743d8e6d67/libcst-1.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da9d7dc83801aba3b8d911f82dc1a375db0d508318bad79d9fb245374afe068", size = 2248523 },
-    { url = "https://files.pythonhosted.org/packages/8c/02/1c9c908724c732f09b11493ee5d61893060ecc9a3dc4bc80032d1be87b37/libcst-1.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c54aa66c86d8ece9c93156a2cf5ca512b0dce40142fe9e072c86af2bf892411", size = 2326040 },
-    { url = "https://files.pythonhosted.org/packages/04/32/7345f10a2dc728015920d689d5c1b8dc0232db321e172cdad2611e73c5b3/libcst-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:62e2682ee1567b6a89c91853865372bf34f178bfd237853d84df2b87b446e654", size = 2026263 },
+sdist = { url = "https://files.pythonhosted.org/packages/4d/c4/5577b92173199299e0d32404aa92a156d353d6ec0f74148f6e418e0defef/libcst-1.5.0.tar.gz", hash = "sha256:8478abf21ae3861a073e898d80b822bd56e578886331b33129ba77fec05b8c24", size = 772970 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/85/44/c8f1e0d83bbdabc240c05d5bedddfd4e095a0031b8df473d8eb004f12554/libcst-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23d0e07fd3ed11480f8993a1e99d58a45f914a711b14f858b8db08ae861a8a34", size = 2112640 },
+    { url = "https://files.pythonhosted.org/packages/20/d5/3d5819da92a8f997ecf0b5a77d65865d4d2aa4209b34e32835b555218689/libcst-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d92c5ae2e2dc9356ad7e3d05077d9b7e5065423e45788fd86729c88729e45c6e", size = 2026866 },
+    { url = "https://files.pythonhosted.org/packages/74/19/d2ebded5990f2f5ab4c86412df75338b9d8b386fbb5e430669f287bc8d9c/libcst-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96adc45e96476350df6b8a5ddbb1e1d6a83a7eb3f13087e52eb7cd2f9b65bcc7", size = 2203742 },
+    { url = "https://files.pythonhosted.org/packages/87/98/d47a9a88df48cc33db7e1219cd7c29bfdfd8d695634f3f2e86ff04bbd58d/libcst-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5978fd60c66794bb60d037b2e6427ea52d032636e84afce32b0f04e1cf500a", size = 2253801 },
+    { url = "https://files.pythonhosted.org/packages/b8/ca/7fdcbab8f8e8c46336099af7929d0f0e5873222830010aae0160d16544c1/libcst-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6502aeb11412afc759036160c686be1107eb5a4466db56b207c786b9b4da7c4", size = 2324610 },
+    { url = "https://files.pythonhosted.org/packages/24/fb/db7c696b7bf8e295aa9bf37091fbd1bad35e491be44926da2b20907c3452/libcst-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cccfc0a78e110c0d0a9d2c6fdeb29feb5274c9157508a8baef7edf352420f6d", size = 2030364 },
+    { url = "https://files.pythonhosted.org/packages/b5/82/5b9d1f89bdba4106de6080ab3384157581af4f0b94e04a7150b917b5b945/libcst-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:585b3aa705b3767d717d2100935d8ef557275ecdd3fac81c3e28db0959efb0ea", size = 2112655 },
+    { url = "https://files.pythonhosted.org/packages/17/4d/c6ed4323e77717edf3f47af8cabbdd4a7de7983fc5a1cc20130947f65f9d/libcst-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8935dd3393e30c2f97344866a4cb14efe560200e232166a8db1de7865c2ef8b2", size = 2026906 },
+    { url = "https://files.pythonhosted.org/packages/eb/ad/10cffc6a69da4320cc75f7f031a48292b61ad5ba0ba94fa9f963cb0b5f67/libcst-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc80ea16c7d44e38f193e4d4ef7ff1e0ba72d8e60e8b61ac6f4c87f070a118bd", size = 2203824 },
+    { url = "https://files.pythonhosted.org/packages/e8/88/016b3feb75a3b16896e27691439c3bd493ae7d896bb4e31d6bd4c2e5c20b/libcst-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02be4aab728261bb76d16e77c9a457884cebb60d09c8edee844de43b0e08aff7", size = 2253854 },
+    { url = "https://files.pythonhosted.org/packages/69/8e/5a60d53493e259743fd574abe442dd7f3b497ebb58dee168473a03f90d3e/libcst-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8fcd78be4d9ce3c36d0c5d0bdd384e0c7d5f72970a9e4ebd56070141972b4ad", size = 2324725 },
+    { url = "https://files.pythonhosted.org/packages/65/86/ddf0d593f4ef5994f456e00e99a1eb28b661aab5df960034199f4d8bbeb4/libcst-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:52b6aadfe54e3ae52c3b815eaaa17ba4da9ff010d5e8adf6a70697872886dd10", size = 2030364 },
+    { url = "https://files.pythonhosted.org/packages/a7/23/9cdb3362ad75490108a03abeaae8d7f7fb0d86586d806102ae9d9690d6b8/libcst-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83bc5fbe34d33597af1d5ea113dcb9b5dd5afe5a5f4316bac4293464d5e3971a", size = 2108563 },
+    { url = "https://files.pythonhosted.org/packages/48/ec/4a1a34c3dbe6d51815700a0c14991f4124f10e82f9959d4fb5a9b0b06c74/libcst-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f10124bf99a0b075eae136ef0ce06204e5f6b8da4596a9c4853a0663e80ddf3", size = 2024056 },
+    { url = "https://files.pythonhosted.org/packages/da/b7/1976377c19f9477267daac2ea8e2d5a72ce12d5b523ff147d404fb7ae74e/libcst-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e581af6127c5af4c9f483e5986d94f0c6b2366967ee134f0a8eba0aa4c8c12", size = 2199473 },
+    { url = "https://files.pythonhosted.org/packages/63/c4/e056f3f34642f294421bd4a4d4b40aeccaf153a456bcb4d7e54f4337143f/libcst-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dba93cca0a5c6d771ed444c44d21ce8ea9b277af7036cea3743677aba9fbbb8", size = 2251411 },
+    { url = "https://files.pythonhosted.org/packages/e8/d6/574fc6c8b0ca81586ee05f284ef6987730b841b31ce246ef9d3c45f17ec4/libcst-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b5c4d87721a7bab265c202575809b810815ab81d5e2e7a5d4417a087975840", size = 2323144 },
+    { url = "https://files.pythonhosted.org/packages/b1/92/5cb62834eec397f4b3218c03acc28b6b8470f87c8dad9e9b0fd738c3948c/libcst-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:b48bf71d52c1e891a0948465a94d9817b5fc1ec1a09603566af90585f3b11948", size = 2029603 },
+    { url = "https://files.pythonhosted.org/packages/60/5e/dd156f628fed03a273d995008f1669e1964727df6a8818bbedaac51f9ae5/libcst-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:88520b6dea59eaea0cae80f77c0a632604a82c5b2d23dedb4b5b34035cbf1615", size = 2108562 },
+    { url = "https://files.pythonhosted.org/packages/2c/54/f63bf0bd2d70179e0557c9474a0511e33e646d398945b5a01de36237ce60/libcst-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:208ea92d80b2eeed8cbc879d5f39f241582a5d56b916b1b65ed2be2f878a2425", size = 2024057 },
+    { url = "https://files.pythonhosted.org/packages/dc/37/ce62947fd7305fb501589e4b8f6e82e3cf61fca2d62392e281c17a2112f5/libcst-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4592872aaf5b7fa5c2727a7d73c0985261f1b3fe7eff51f4fd5b8174f30b4e2", size = 2199474 },
+    { url = "https://files.pythonhosted.org/packages/c9/95/b878c95af17f3e341ac5dc18e3160d45d86b2c05a0cafd866ceb0b766bbd/libcst-1.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2788b2b5838b78fe15df8e9fa6b6903195ea49b2d2ba43e8f423f6c90e4b69f", size = 2251410 },
+    { url = "https://files.pythonhosted.org/packages/e1/26/697b54aa839c4dc6ea2787d5e977ed4be0636149f85df1a0cba7a29bd188/libcst-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5b5bcd3a9ba92840f27ad34eaa038acbee195ec337da39536c0a2efbbf28efd", size = 2323144 },
+    { url = "https://files.pythonhosted.org/packages/a0/9f/5b5481d716670ed5fbd8d06dfa94b7108272b645da2f2406eb909cb6a450/libcst-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d6acb0bdee1e55b44c6215c59755ec4693ac01e74bb1fde04c37358b378835d", size = 2029600 },
 ]
 
 [[package]]
@@ -1608,6 +1614,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d4/55/90db48d85f7689ec6f81c0db0622d704306c5284850383c090e6c7195a5c/pip-24.2-py3-none-any.whl", hash = "sha256:2cd581cf58ab7fcfca4ce8efa6dcacd0de5bf8d0a3eb9ec927e07405f4d9e2a2", size = 1815170 },
 ]
 
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
 [[package]]
 name = "pluggy"
 version = "1.5.0"
@@ -1834,16 +1849,17 @@ wheels = [
 
 [[package]]
 name = "pydantic-pkgr"
-version = "0.4.24"
+version = "0.5.2"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
+    { name = "platformdirs" },
     { name = "pydantic" },
     { name = "pydantic-core" },
     { name = "typing-extensions" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/14/65/9a3d801d19686de0d940d131a135bdb1a79ed6ac430c0f7901dcd45f710a/pydantic_pkgr-0.4.24.tar.gz", hash = "sha256:5a4de016478ecd7c0aec0818f5d2e72255a2f625a5c200af217aa70c4524fa90", size = 38785 }
+sdist = { url = "https://files.pythonhosted.org/packages/0c/6c/ed0e6d519ecd4ac7cb36c8d74344a26260f7f1878c590a9f3cfb34057bec/pydantic_pkgr-0.5.2.tar.gz", hash = "sha256:8cd01cef9db94e6b97222c1e44b8a7a4b73ca0b8c51a7fddece501094c422d3e", size = 42303 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/13/0d/84080a63ebcb112fe7a96bc681aee91ccdf0c334598e2016984aa4c74d6e/pydantic_pkgr-0.4.24-py3-none-any.whl", hash = "sha256:cec121b5b0fc73421af9915e45dfb09cdc776734fcee87f08b501053f5a36259", size = 41791 },
+    { url = "https://files.pythonhosted.org/packages/c0/94/1db0817fd8fa234c9696625e314e44b91a9cee7f37cc715328cd57d2454d/pydantic_pkgr-0.5.2-py3-none-any.whl", hash = "sha256:7511830af65a75c03d9e4320d73640429ae53c1f1c2d39f28067857369f142fd", size = 45050 },
 ]
 
 [[package]]
@@ -2301,7 +2317,7 @@ wheels = [
 
 [[package]]
 name = "sphinx"
-version = "8.0.2"
+version = "8.1.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "alabaster" },
@@ -2322,9 +2338,9 @@ dependencies = [
     { name = "sphinxcontrib-serializinghtml" },
     { name = "tomli", marker = "python_full_version < '3.11'" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/25/a7/3cc3d6dcad70aba2e32a3ae8de5a90026a0a2fdaaa0756925e3a120249b6/sphinx-8.0.2.tar.gz", hash = "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b", size = 8189041 }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/f3/e3c6fb6d015d6b0c5215d1a6e45276aa89b6685fc63a1b7ac230bcebcb4f/sphinx-8.1.0.tar.gz", hash = "sha256:109454425dbf4c78ecfdd481e56f078376d077edbda29804dba05c5161c8de06", size = 8183960 }
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/4d/61/2ad169c6ff1226b46e50da0e44671592dbc6d840a52034a0193a99b28579/sphinx-8.0.2-py3-none-any.whl", hash = "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d", size = 3498950 },
+    { url = "https://files.pythonhosted.org/packages/fb/21/143e5e4666432668fbd669f89ee0abc50040787f932bd30befd0f7a42a6e/sphinx-8.1.0-py3-none-any.whl", hash = "sha256:3202bba95697b9fc4371a07d6d457239de9860244ce235283149f817c253fd2f", size = 3486829 },
 ]
 
 [[package]]
@@ -2829,32 +2845,35 @@ wheels = [
 
 [[package]]
 name = "zope-interface"
-version = "7.0.3"
+version = "7.1.0"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "setuptools" },
 ]
-sdist = { url = "https://files.pythonhosted.org/packages/c8/83/7de03efae7fc9a4ec64301d86e29a324f32fe395022e3a5b1a79e376668e/zope.interface-7.0.3.tar.gz", hash = "sha256:cd2690d4b08ec9eaf47a85914fe513062b20da78d10d6d789a792c0b20307fb1", size = 252504 }
-wheels = [
-    { url = "https://files.pythonhosted.org/packages/e9/33/a55311169d3d41b61da7c5b7d528ebb0469263252a71d9510849c0d66201/zope.interface-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b9369671a20b8d039b8e5a1a33abd12e089e319a3383b4cc0bf5c67bd05fe7b", size = 207912 },
-    { url = "https://files.pythonhosted.org/packages/6b/c3/7d18af6971634087a4ddc436e37fc47988c31635cd01948ff668d11c96c4/zope.interface-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:db6237e8fa91ea4f34d7e2d16d74741187e9105a63bbb5686c61fea04cdbacca", size = 208416 },
-    { url = "https://files.pythonhosted.org/packages/8a/64/2922134a93978b6a8b823f3e784d6af3d5d165fad1f66388b0f89b5695fc/zope.interface-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53d678bb1c3b784edbfb0adeebfeea6bf479f54da082854406a8f295d36f8386", size = 254614 },
-    { url = "https://files.pythonhosted.org/packages/5a/a9/9665ba3aa7c6173ea2c3249c85546139119eaf3146f280cea8053e0047b9/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3aa8fcbb0d3c2be1bfd013a0f0acd636f6ed570c287743ae2bbd467ee967154d", size = 249026 },
-    { url = "https://files.pythonhosted.org/packages/45/58/890cf943c9a7dd82d096a11872c7efb3f0e97e86f71b886018044fb01972/zope.interface-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6195c3c03fef9f87c0dbee0b3b6451df6e056322463cf35bca9a088e564a3c58", size = 254134 },
-    { url = "https://files.pythonhosted.org/packages/f9/41/b126c98cc8a72b807cecab5ba483444e573ef9c7ca7f71449e96afd14d4d/zope.interface-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:11fa1382c3efb34abf16becff8cb214b0b2e3144057c90611621f2d186b7e1b7", size = 211591 },
-    { url = "https://files.pythonhosted.org/packages/80/ff/66b5cd662b177de4082cac412a877c7a528ef79a392d90e504f50c041dda/zope.interface-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:af94e429f9d57b36e71ef4e6865182090648aada0cb2d397ae2b3f7fc478493a", size = 208441 },
-    { url = "https://files.pythonhosted.org/packages/c1/a3/a890f35a62aa25233c95e2af4510aa1df0553be48450bb0840b8d3b2a62c/zope.interface-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dd647fcd765030638577fe6984284e0ebba1a1008244c8a38824be096e37fe3", size = 208954 },
-    { url = "https://files.pythonhosted.org/packages/9e/1b/79bcfbdc7d621c410a188f25d78f6e07aff7f608c9589cfba77003769f98/zope.interface-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bee1b722077d08721005e8da493ef3adf0b7908e0cd85cc7dc836ac117d6f32", size = 261132 },
-    { url = "https://files.pythonhosted.org/packages/c6/91/d3e665df6837629e2eec9cdc8cd1118f1a0e74b586bbec2e6cfc6a1b6c3a/zope.interface-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2545d6d7aac425d528cd9bf0d9e55fcd47ab7fd15f41a64b1c4bf4c6b24946dc", size = 255243 },
-    { url = "https://files.pythonhosted.org/packages/2c/c2/39964ef5fed7ac1523bab2d1bba244290965da6f720164b603ec07adf0a7/zope.interface-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d04b11ea47c9c369d66340dbe51e9031df2a0de97d68f442305ed7625ad6493", size = 259957 },
-    { url = "https://files.pythonhosted.org/packages/6b/68/3937ac6cd0299694102d71721efd38fd1ceba7eaef20aefed3cdbb22527c/zope.interface-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:064ade95cb54c840647205987c7b557f75d2b2f7d1a84bfab4cf81822ef6e7d1", size = 211972 },
-    { url = "https://files.pythonhosted.org/packages/ec/be/6640eb57c4b84a471d691082d0207434d1524e428fba1231c335a4cad446/zope.interface-7.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3fcdc76d0cde1c09c37b7c6b0f8beba2d857d8417b055d4f47df9c34ec518bdd", size = 208567 },
-    { url = "https://files.pythonhosted.org/packages/2d/45/a891ee78ba5ef5b5437394f8c2c56c094ed1ab41a80ef7afe50191dce3d2/zope.interface-7.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d4b91821305c8d8f6e6207639abcbdaf186db682e521af7855d0bea3047c8ca", size = 208972 },
-    { url = "https://files.pythonhosted.org/packages/14/44/d12683e823ced271ae2ca3976f16066634911e02540a9559b09444a4b2d3/zope.interface-7.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35062d93bc49bd9b191331c897a96155ffdad10744ab812485b6bad5b588d7e4", size = 266389 },
-    { url = "https://files.pythonhosted.org/packages/db/35/c83308ac84552c2242d5d59488dbea9a91c64765e117a71c566ddf896e31/zope.interface-7.0.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c96b3e6b0d4f6ddfec4e947130ec30bd2c7b19db6aa633777e46c8eecf1d6afd", size = 261112 },
-    { url = "https://files.pythonhosted.org/packages/3d/ed/0ac414f9373d742d2eb2f436b595ed281031780a405621a4d906096092ea/zope.interface-7.0.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0c151a6c204f3830237c59ee4770cc346868a7a1af6925e5e38650141a7f05", size = 267044 },
-    { url = "https://files.pythonhosted.org/packages/38/92/e9fe2a8cb53cffc73f923da84e50e0ee3a8d38a64bef6965428d5b5c4910/zope.interface-7.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:3de1d553ce72868b77a7e9d598c9bff6d3816ad2b4cc81c04f9d8914603814f3", size = 212064 },
-    { url = "https://files.pythonhosted.org/packages/2b/6f/059521297028f3037f2b19a711be845983151acbdeda1031749a91d07048/zope.interface-7.0.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab985c566a99cc5f73bc2741d93f1ed24a2cc9da3890144d37b9582965aff996", size = 266369 },
-    { url = "https://files.pythonhosted.org/packages/ce/bb/51ab7785b2ad3123d5eb85b548f98fe2c0809c6bd452e677b1aca71c3c79/zope.interface-7.0.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d976fa7b5faf5396eb18ce6c132c98e05504b52b60784e3401f4ef0b2e66709b", size = 261119 },
-    { url = "https://files.pythonhosted.org/packages/be/56/6a57ef0699b857b33a407162f29eade4062596870d335f53e914bb98fd0e/zope.interface-7.0.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a207c6b2c58def5011768140861a73f5240f4f39800625072ba84e76c9da0b", size = 267059 },
+sdist = { url = "https://files.pythonhosted.org/packages/e4/1f/8bb0739aba9a8909bcfa2e12dc20443ebd5bd773b6796603f1a126211e18/zope_interface-7.1.0.tar.gz", hash = "sha256:3f005869a1a05e368965adb2075f97f8ee9a26c61898a9e52a9764d93774f237", size = 300239 }
+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 },
 ]