mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2025-05-28 05:34:14 -04:00
add BaseHook concept to underlie all Plugin hooks
This commit is contained in:
parent
ed5357cec9
commit
44669fab73
12 changed files with 212 additions and 79 deletions
|
@ -1,14 +1,14 @@
|
|||
from typing import List, Type, Any
|
||||
|
||||
from pydantic_core import core_schema
|
||||
from pydantic import GetCoreSchemaHandler
|
||||
from pydantic import GetCoreSchemaHandler, BaseModel
|
||||
|
||||
from django.utils.functional import classproperty
|
||||
from django.core.checks import Warning, Tags, register
|
||||
|
||||
class BaseCheck:
|
||||
label: str = ''
|
||||
tag = Tags.database
|
||||
tag: str = Tags.database
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(cls, source_type: Any, handler: GetCoreSchemaHandler) -> core_schema.CoreSchema:
|
||||
|
|
|
@ -5,6 +5,7 @@ from typing import Optional, List, Literal
|
|||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field, ConfigDict, computed_field
|
||||
|
||||
from .base_hook import BaseHook, HookType
|
||||
|
||||
ConfigSectionName = Literal[
|
||||
'GENERAL_CONFIG',
|
||||
|
@ -20,24 +21,26 @@ ConfigSectionNames: List[ConfigSectionName] = [
|
|||
]
|
||||
|
||||
|
||||
class BaseConfigSet(BaseModel):
|
||||
class BaseConfigSet(BaseHook):
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True, extra='allow', populate_by_name=True)
|
||||
hook_type: HookType = 'CONFIG'
|
||||
|
||||
section: ConfigSectionName = 'GENERAL_CONFIG'
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.__class__.__name__
|
||||
|
||||
def register(self, settings, parent_plugin=None):
|
||||
"""Installs the ConfigSet into Django settings.CONFIGS (and settings.HOOKS)."""
|
||||
if settings is None:
|
||||
from django.conf import settings as django_settings
|
||||
settings = django_settings
|
||||
|
||||
self._plugin = parent_plugin # for debugging only, never rely on this!
|
||||
|
||||
# install hook into settings.CONFIGS
|
||||
settings.CONFIGS[self.name] = self
|
||||
|
||||
# record installed hook in settings.HOOKS
|
||||
super().register(settings, parent_plugin=parent_plugin)
|
||||
|
||||
|
||||
|
||||
# class WgetToggleConfig(ConfigSet):
|
||||
|
|
71
archivebox/plugantic/base_hook.py
Normal file
71
archivebox/plugantic/base_hook.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
__package__ = 'archivebox.plugantic'
|
||||
|
||||
import json
|
||||
from typing import Optional, List, Literal, ClassVar
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel, Field, ConfigDict, computed_field
|
||||
|
||||
|
||||
HookType = Literal['CONFIG', 'BINPROVIDER', 'BINARY', 'EXTRACTOR', 'REPLAYER', 'CHECK', 'ADMINDATAVIEW']
|
||||
hook_type_names: List[HookType] = ['CONFIG', 'BINPROVIDER', 'BINARY', 'EXTRACTOR', 'REPLAYER', 'CHECK', 'ADMINDATAVIEW']
|
||||
|
||||
|
||||
|
||||
class BaseHook(BaseModel):
|
||||
"""
|
||||
A Plugin consists of a list of Hooks, applied to django.conf.settings when AppConfig.read() -> Plugin.register() is called.
|
||||
Plugin.register() then calls each Hook.register() on the provided settings.
|
||||
each Hook.regsiter() function (ideally pure) takes a django.conf.settings as input and returns a new one back.
|
||||
or
|
||||
it modifies django.conf.settings in-place to add changes corresponding to its HookType.
|
||||
e.g. for a HookType.CONFIG, the Hook.register() function places the hook in settings.CONFIG (and settings.HOOKS)
|
||||
An example of an impure Hook would be a CHECK that modifies settings but also calls django.core.checks.register(check).
|
||||
|
||||
|
||||
setup_django() -> imports all settings.INSTALLED_APPS...
|
||||
# django imports AppConfig, models, migrations, admins, etc. for all installed apps
|
||||
# django then calls AppConfig.ready() on each installed app...
|
||||
|
||||
builtin_plugins.npm.NpmPlugin().AppConfig.ready() # called by django
|
||||
builtin_plugins.npm.NpmPlugin().register(settings) ->
|
||||
builtin_plugins.npm.NpmConfigSet().register(settings)
|
||||
plugantic.base_configset.BaseConfigSet().register(settings)
|
||||
plugantic.base_hook.BaseHook().register(settings, parent_plugin=builtin_plugins.npm.NpmPlugin())
|
||||
|
||||
...
|
||||
...
|
||||
|
||||
|
||||
"""
|
||||
model_config = ConfigDict(
|
||||
extra='allow',
|
||||
arbitrary_types_allowed=True,
|
||||
from_attributes=True,
|
||||
populate_by_name=True,
|
||||
validate_defaults=True,
|
||||
validate_assignment=True,
|
||||
)
|
||||
|
||||
hook_type: HookType = 'CONFIG'
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return f'{self.__module__}.{__class__.__name__}'
|
||||
|
||||
def register(self, settings, parent_plugin=None):
|
||||
"""Load a record of an installed hook into global Django settings.HOOKS at runtime."""
|
||||
|
||||
if settings is None:
|
||||
from django.conf import settings as django_settings
|
||||
settings = django_settings
|
||||
|
||||
assert json.dumps(self.model_json_schema(), indent=4), f'Hook {self.name} has invalid JSON schema.'
|
||||
|
||||
self._plugin = parent_plugin # for debugging only, never rely on this!
|
||||
|
||||
# record installed hook in settings.HOOKS
|
||||
settings.HOOKS[self.name] = self
|
||||
|
||||
hook_prefix, plugin_shortname = self.name.split('.', 1)
|
||||
|
||||
print('REGISTERED HOOK:', self.name)
|
|
@ -1,6 +1,8 @@
|
|||
__package__ = 'archivebox.plugantic'
|
||||
|
||||
import json
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.core.checks import register
|
||||
|
@ -32,12 +34,11 @@ 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'
|
||||
name: str = Field() # e.g. 'builtin_plugins.singlefile' (DottedImportPath)
|
||||
app_label: str = Field() # e.g. 'singlefile' (one-word machine-readable representation, to use as url-safe id/db-table prefix_/attr name)
|
||||
verbose_name: str = Field() # e.g. 'SingleFile' (human-readable *short* label, for use in column names, form labels, etc.)
|
||||
|
||||
# Required by Plugantic:
|
||||
# All the hooks the plugin will install:
|
||||
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')]
|
||||
|
@ -53,20 +54,23 @@ class BasePlugin(BaseModel):
|
|||
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.'
|
||||
return self
|
||||
|
||||
@property
|
||||
def AppConfig(plugin_self) -> Type[AppConfig]:
|
||||
"""Generate a Django AppConfig class for this plugin."""
|
||||
|
||||
class PluginAppConfig(AppConfig):
|
||||
"""Django AppConfig for plugin, allows it to be loaded as a Django app listed in settings.INSTALLED_APPS."""
|
||||
name = plugin_self.name
|
||||
app_label = plugin_self.app_label
|
||||
verbose_name = plugin_self.verbose_name
|
||||
default_auto_field = 'django.db.models.AutoField'
|
||||
|
||||
def ready(self):
|
||||
from django.conf import settings
|
||||
|
||||
plugin_self.validate()
|
||||
# plugin_self.validate()
|
||||
plugin_self.register(settings)
|
||||
|
||||
return PluginAppConfig
|
||||
|
@ -105,11 +109,6 @@ class BasePlugin(BaseModel):
|
|||
@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."""
|
||||
|
@ -185,6 +184,20 @@ class BasePlugin(BaseModel):
|
|||
# 'binaries': new_binaries,
|
||||
# })
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def module_dir(self) -> Path:
|
||||
return Path(inspect.getfile(self.__class__)).parent.resolve()
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def module_path(self) -> str: # DottedImportPath
|
||||
""""
|
||||
Dotted import path of the plugin's module (after its loaded via settings.INSTALLED_APPS).
|
||||
e.g. 'archivebox.builtin_plugins.npm'
|
||||
"""
|
||||
return self.name.strip('archivebox.')
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue