|
|
@@ -0,0 +1,202 @@
|
|
|
+__package__ = 'archivebox.plugantic'
|
|
|
+
|
|
|
+import json
|
|
|
+
|
|
|
+from django.apps import AppConfig
|
|
|
+from django.core.checks import register
|
|
|
+
|
|
|
+from typing import List, ClassVar, Type, Dict
|
|
|
+from typing_extensions import Self
|
|
|
+
|
|
|
+from pydantic import (
|
|
|
+ BaseModel,
|
|
|
+ ConfigDict,
|
|
|
+ Field,
|
|
|
+ model_validator,
|
|
|
+ InstanceOf,
|
|
|
+ computed_field,
|
|
|
+ validate_call,
|
|
|
+)
|
|
|
+
|
|
|
+from .base_configset import BaseConfigSet
|
|
|
+from .base_binary import BaseBinProvider, BaseBinary
|
|
|
+from .base_extractor import BaseExtractor
|
|
|
+from .base_replayer import BaseReplayer
|
|
|
+from .base_check import BaseCheck
|
|
|
+from .base_admindataview import BaseAdminDataView
|
|
|
+
|
|
|
+from ..config import ANSI, AttrDict
|
|
|
+
|
|
|
+
|
|
|
+class BasePlugin(BaseModel):
|
|
|
+ model_config = ConfigDict(arbitrary_types_allowed=True, extra='ignore', populate_by_name=True)
|
|
|
+
|
|
|
+ # Required by AppConfig:
|
|
|
+ name: str = Field() # e.g. 'builtin_plugins.singlefile'
|
|
|
+ app_label: str = Field() # e.g. 'singlefile'
|
|
|
+ verbose_name: str = Field() # e.g. 'SingleFile'
|
|
|
+ default_auto_field: ClassVar[str] = 'django.db.models.AutoField'
|
|
|
+
|
|
|
+ # Required by Plugantic:
|
|
|
+ configs: List[InstanceOf[BaseConfigSet]] = Field(default=[])
|
|
|
+ binproviders: List[InstanceOf[BaseBinProvider]] = Field(default=[]) # e.g. [Binary(name='yt-dlp')]
|
|
|
+ binaries: List[InstanceOf[BaseBinary]] = Field(default=[]) # e.g. [Binary(name='yt-dlp')]
|
|
|
+ extractors: List[InstanceOf[BaseExtractor]] = Field(default=[])
|
|
|
+ replayers: List[InstanceOf[BaseReplayer]] = Field(default=[])
|
|
|
+ checks: List[InstanceOf[BaseCheck]] = Field(default=[])
|
|
|
+ admindataviews: List[InstanceOf[BaseAdminDataView]] = Field(default=[])
|
|
|
+
|
|
|
+ @model_validator(mode='after')
|
|
|
+ def validate(self) -> Self:
|
|
|
+ """Validate the plugin's build-time configuration here before it's registered in Django at runtime."""
|
|
|
+
|
|
|
+ assert self.name and self.app_label and self.verbose_name, f'{self.__class__.__name__} is missing .name or .app_label or .verbose_name'
|
|
|
+
|
|
|
+ assert json.dumps(self.model_json_schema(), indent=4), f'Plugin {self.name} has invalid JSON schema.'
|
|
|
+
|
|
|
+ @property
|
|
|
+ def AppConfig(plugin_self) -> Type[AppConfig]:
|
|
|
+ """Generate a Django AppConfig class for this plugin."""
|
|
|
+
|
|
|
+ class PluginAppConfig(AppConfig):
|
|
|
+ name = plugin_self.name
|
|
|
+ app_label = plugin_self.app_label
|
|
|
+ verbose_name = plugin_self.verbose_name
|
|
|
+
|
|
|
+ def ready(self):
|
|
|
+ from django.conf import settings
|
|
|
+
|
|
|
+ plugin_self.validate()
|
|
|
+ plugin_self.register(settings)
|
|
|
+
|
|
|
+ return PluginAppConfig
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def BINPROVIDERS(self) -> Dict[str, BaseBinProvider]:
|
|
|
+ return AttrDict({binprovider.name: binprovider for binprovider in self.binproviders})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def BINARIES(self) -> Dict[str, BaseBinary]:
|
|
|
+ return AttrDict({binary.python_name: binary for binary in self.binaries})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def CONFIGS(self) -> Dict[str, BaseConfigSet]:
|
|
|
+ return AttrDict({config.name: config for config in self.configs})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def EXTRACTORS(self) -> Dict[str, BaseExtractor]:
|
|
|
+ return AttrDict({extractor.name: extractor for extractor in self.extractors})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def REPLAYERS(self) -> Dict[str, BaseReplayer]:
|
|
|
+ return AttrDict({replayer.name: replayer for replayer in self.replayers})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def CHECKS(self) -> Dict[str, BaseCheck]:
|
|
|
+ return AttrDict({check.name: check for check in self.checks})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def ADMINDATAVIEWS(self) -> Dict[str, BaseCheck]:
|
|
|
+ return AttrDict({admindataview.name: admindataview for admindataview in self.admindataviews})
|
|
|
+
|
|
|
+ @computed_field
|
|
|
+ @property
|
|
|
+ def PLUGIN_KEYS(self) -> List[str]:
|
|
|
+ return
|
|
|
+
|
|
|
+ def register(self, settings=None):
|
|
|
+ """Loads this plugin's configs, binaries, extractors, and replayers into global Django settings at runtime."""
|
|
|
+
|
|
|
+ if settings is None:
|
|
|
+ from django.conf import settings as django_settings
|
|
|
+ settings = django_settings
|
|
|
+
|
|
|
+ assert all(hasattr(settings, key) for key in ['PLUGINS', 'CONFIGS', 'BINARIES', 'EXTRACTORS', 'REPLAYERS', 'ADMINDATAVIEWS']), 'Tried to register plugin in settings but couldnt find required global dicts in settings.'
|
|
|
+
|
|
|
+ assert json.dumps(self.model_json_schema(), indent=4), f'Plugin {self.name} has invalid JSON schema.'
|
|
|
+
|
|
|
+ assert self.app_label not in settings.PLUGINS, f'Tried to register plugin {self.name} but it conflicts with existing plugin of the same name ({self.app_label}).'
|
|
|
+
|
|
|
+ ### Mutate django.conf.settings... values in-place to include plugin-provided overrides
|
|
|
+ settings.PLUGINS[self.app_label] = self
|
|
|
+
|
|
|
+ for config in self.CONFIGS.values():
|
|
|
+ config.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for binprovider in self.BINPROVIDERS.values():
|
|
|
+ binprovider.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for binary in self.BINARIES.values():
|
|
|
+ binary.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for extractor in self.EXTRACTORS.values():
|
|
|
+ extractor.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for replayer in self.REPLAYERS.values():
|
|
|
+ replayer.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for check in self.CHECKS.values():
|
|
|
+ check.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ for admindataview in self.ADMINDATAVIEWS.values():
|
|
|
+ admindataview.register(settings, parent_plugin=self)
|
|
|
+
|
|
|
+ # TODO: add parsers? custom templates? persona fixtures?
|
|
|
+
|
|
|
+ plugin_prefix, plugin_shortname = self.name.split('.', 1)
|
|
|
+
|
|
|
+ print(
|
|
|
+ f' > {ANSI.black}{plugin_prefix.upper().replace("_PLUGINS", "").ljust(15)} ' +
|
|
|
+ f'{ANSI.lightyellow}{plugin_shortname.ljust(12)} ' +
|
|
|
+ f'{ANSI.black}CONFIGSx{len(self.configs)} BINARIESx{len(self.binaries)} EXTRACTORSx{len(self.extractors)} REPLAYERSx{len(self.replayers)} CHECKSx{len(self.CHECKS)} ADMINDATAVIEWSx{len(self.ADMINDATAVIEWS)}{ANSI.reset}'
|
|
|
+ )
|
|
|
+
|
|
|
+ # @validate_call
|
|
|
+ # def install_binaries(self) -> Self:
|
|
|
+ # new_binaries = []
|
|
|
+ # for idx, binary in enumerate(self.binaries):
|
|
|
+ # new_binaries.append(binary.install() or binary)
|
|
|
+ # return self.model_copy(update={
|
|
|
+ # 'binaries': new_binaries,
|
|
|
+ # })
|
|
|
+
|
|
|
+ @validate_call
|
|
|
+ def load_binaries(self, cache=True) -> Self:
|
|
|
+ new_binaries = []
|
|
|
+ for idx, binary in enumerate(self.binaries):
|
|
|
+ new_binaries.append(binary.load(cache=cache) or binary)
|
|
|
+ return self.model_copy(update={
|
|
|
+ 'binaries': new_binaries,
|
|
|
+ })
|
|
|
+
|
|
|
+ # @validate_call
|
|
|
+ # def load_or_install_binaries(self, cache=True) -> Self:
|
|
|
+ # new_binaries = []
|
|
|
+ # for idx, binary in enumerate(self.binaries):
|
|
|
+ # new_binaries.append(binary.load_or_install(cache=cache) or binary)
|
|
|
+ # return self.model_copy(update={
|
|
|
+ # 'binaries': new_binaries,
|
|
|
+ # })
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+# class YtdlpPlugin(BasePlugin):
|
|
|
+# name: str = 'ytdlp'
|
|
|
+# configs: List[SerializeAsAny[BaseConfigSet]] = []
|
|
|
+# binaries: List[SerializeAsAny[BaseBinary]] = [YtdlpBinary()]
|
|
|
+# extractors: List[SerializeAsAny[BaseExtractor]] = [YtdlpExtractor()]
|
|
|
+# replayers: List[SerializeAsAny[BaseReplayer]] = [MEDIA_REPLAYER]
|
|
|
+
|
|
|
+# class WgetPlugin(BasePlugin):
|
|
|
+# name: str = 'wget'
|
|
|
+# configs: List[SerializeAsAny[BaseConfigSet]] = [*WGET_CONFIG]
|
|
|
+# binaries: List[SerializeAsAny[BaseBinary]] = [WgetBinary()]
|
|
|
+# extractors: List[SerializeAsAny[BaseExtractor]] = [WgetExtractor(), WarcExtractor()]
|