added notifier/provider structure, moved addon out
now treepuncher has 1 notifier, which holds many providers for various services. Each provider is an Addon, but not the Notifier itself. Moved Addon and Notifier out of Treepuncher, and added type hints without dependancy cycle with TYPE_CHECKING control
This commit is contained in:
parent
1307869d9c
commit
cfd5dd079a
5 changed files with 175 additions and 120 deletions
|
@ -1 +1,4 @@
|
||||||
from .treepuncher import Treepuncher, ConfigObject, Addon, Notifier
|
from .scaffold import ConfigObject
|
||||||
|
from .treepuncher import Treepuncher
|
||||||
|
from .addon import Addon
|
||||||
|
from .notifier import Notifier, Provider
|
||||||
|
|
87
src/treepuncher/addon.py
Normal file
87
src/treepuncher/addon.py
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING, Dict, Any, Union, List, Callable, get_type_hints, get_args, get_origin
|
||||||
|
from dataclasses import dataclass, MISSING, fields
|
||||||
|
|
||||||
|
from .scaffold import ConfigObject
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .treepuncher import Treepuncher
|
||||||
|
|
||||||
|
def parse_with_hint(val:str, hint:Any) -> Any:
|
||||||
|
if hint is bool:
|
||||||
|
if val.lower() in ['1', 'true', 't', 'on', 'enabled']:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
if hint is list or get_origin(hint) is list:
|
||||||
|
if get_args(hint):
|
||||||
|
return list( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
||||||
|
return val.split()
|
||||||
|
if hint is tuple or get_origin(hint) is tuple:
|
||||||
|
if get_args(hint):
|
||||||
|
return tuple( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
||||||
|
return val.split()
|
||||||
|
if hint is set or get_origin(hint) is set:
|
||||||
|
if get_args(hint):
|
||||||
|
return set( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
||||||
|
return set(val.split())
|
||||||
|
if hint is dict or get_origin(hint) is dict:
|
||||||
|
return json.loads(val)
|
||||||
|
if hint is Union or get_origin(hint) is Union:
|
||||||
|
# TODO str will never fail, should be tried last.
|
||||||
|
# cheap fix: sort keys by name so that "str" comes last
|
||||||
|
for t in sorted(get_args(hint), key=lambda x : str(x)):
|
||||||
|
try:
|
||||||
|
return t(val)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return (get_origin(hint) or hint)(val) # try to instantiate directly
|
||||||
|
|
||||||
|
class Addon:
|
||||||
|
name: str
|
||||||
|
config: ConfigObject
|
||||||
|
logger: logging.Logger
|
||||||
|
|
||||||
|
_client: Treepuncher
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Options(ConfigObject):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> Treepuncher:
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def __init__(self, client: Treepuncher, *args, **kwargs):
|
||||||
|
self._client = client
|
||||||
|
self.name = type(self).__name__
|
||||||
|
cfg = self._client.config
|
||||||
|
opts: Dict[str, Any] = {}
|
||||||
|
cfg_clazz = get_type_hints(type(self))['config']
|
||||||
|
if cfg_clazz is not ConfigObject:
|
||||||
|
for field in fields(cfg_clazz):
|
||||||
|
default = field.default if field.default is not MISSING \
|
||||||
|
else field.default_factory() if field.default_factory is not MISSING \
|
||||||
|
else MISSING
|
||||||
|
if cfg.has_option(self.name, field.name):
|
||||||
|
opts[field.name] = parse_with_hint(self._client.config[self.name].get(field.name), field.type)
|
||||||
|
elif default is MISSING:
|
||||||
|
repr_type = field.type.__name__ if isinstance(field.type, type) else str(field.type) # TODO fix for 3.8 I think?
|
||||||
|
raise ValueError(
|
||||||
|
f"Missing required value '{field.name}' of type '{repr_type}' in section '{self.name}'"
|
||||||
|
)
|
||||||
|
else: # not really necessary since it's a dataclass but whatever
|
||||||
|
opts[field.name] = default
|
||||||
|
self.config = self.Options(**opts)
|
||||||
|
self.logger = self._client.logger.getChild(self.name)
|
||||||
|
self.register()
|
||||||
|
|
||||||
|
def register(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def cleanup(self):
|
||||||
|
pass
|
59
src/treepuncher/notifier.py
Normal file
59
src/treepuncher/notifier.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import List, Callable, Optional, TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .treepuncher import Treepuncher
|
||||||
|
|
||||||
|
from .addon import Addon
|
||||||
|
|
||||||
|
class Provider(Addon):
|
||||||
|
async def notify(self, text, log:bool = False, **kwargs):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
class Notifier:
|
||||||
|
_report_functions : List[Callable]
|
||||||
|
_providers : List[Provider]
|
||||||
|
_client : Treepuncher
|
||||||
|
logger : logging.Logger
|
||||||
|
|
||||||
|
def __init__(self, client:Treepuncher):
|
||||||
|
self._report_functions = []
|
||||||
|
self._providers = []
|
||||||
|
self._client = client
|
||||||
|
self.logger = client.logger.getChild("notifier")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def providers(self) -> List[Provider]:
|
||||||
|
return self._providers
|
||||||
|
|
||||||
|
def add_reporter(self, fn:Callable):
|
||||||
|
self._report_functions.append(fn)
|
||||||
|
return fn
|
||||||
|
|
||||||
|
def add_provider(self, p:Provider):
|
||||||
|
self._providers.append(p)
|
||||||
|
|
||||||
|
def get_provider(self, name:str) -> Optional[Provider]:
|
||||||
|
for p in self.providers:
|
||||||
|
if p.name == name:
|
||||||
|
return p
|
||||||
|
return None
|
||||||
|
|
||||||
|
def report(self) -> str:
|
||||||
|
return '\n'.join(str(fn()).strip() for fn in self._report_functions)
|
||||||
|
|
||||||
|
async def notify(self, text, log:bool = False, **kwargs):
|
||||||
|
self.logger.info("%s %s (%s)", "[n]" if log else "[N]", text, str(kwargs))
|
||||||
|
await asyncio.gather(
|
||||||
|
*(p.notify(text, log=log, **kwargs) for p in self.providers)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
await asyncio.gather(
|
||||||
|
*(p.initialize() for p in self.providers)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
await asyncio.gather(
|
||||||
|
*(p.cleanup() for p in self.providers)
|
||||||
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Type
|
from typing import Type, Any
|
||||||
|
|
||||||
from aiocraft.client import MinecraftClient
|
from aiocraft.client import MinecraftClient
|
||||||
from aiocraft.util import helpers
|
from aiocraft.util import helpers
|
||||||
|
@ -12,6 +12,10 @@ from .traits import CallbacksHolder, Runnable
|
||||||
from .events import ConnectedEvent, DisconnectedEvent
|
from .events import ConnectedEvent, DisconnectedEvent
|
||||||
from .events.base import BaseEvent
|
from .events.base import BaseEvent
|
||||||
|
|
||||||
|
class ConfigObject:
|
||||||
|
def __getitem__(self, key: str) -> Any:
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
class Scaffold(
|
class Scaffold(
|
||||||
MinecraftClient,
|
MinecraftClient,
|
||||||
CallbacksHolder,
|
CallbacksHolder,
|
||||||
|
|
|
@ -16,112 +16,12 @@ from aiocraft.mc.auth import AuthInterface, AuthException, MojangAuthenticator,
|
||||||
|
|
||||||
from .storage import Storage, SystemState, AuthenticatorState
|
from .storage import Storage, SystemState, AuthenticatorState
|
||||||
from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld
|
from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld
|
||||||
|
from .scaffold import ConfigObject
|
||||||
|
from .addon import Addon
|
||||||
|
from .notifier import Notifier, Provider, LoggingProvider
|
||||||
|
|
||||||
__VERSION__ = pkg_resources.get_distribution('treepuncher').version
|
__VERSION__ = pkg_resources.get_distribution('treepuncher').version
|
||||||
|
|
||||||
def parse_with_hint(val:str, hint:Any) -> Any:
|
|
||||||
if hint is bool:
|
|
||||||
if val.lower() in ['1', 'true', 't', 'on', 'enabled']:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
if hint is list or get_origin(hint) is list:
|
|
||||||
if get_args(hint):
|
|
||||||
return list( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
|
||||||
return val.split()
|
|
||||||
if hint is tuple or get_origin(hint) is tuple:
|
|
||||||
if get_args(hint):
|
|
||||||
return tuple( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
|
||||||
return val.split()
|
|
||||||
if hint is set or get_origin(hint) is set:
|
|
||||||
if get_args(hint):
|
|
||||||
return set( parse_with_hint(x, get_args(hint)[0]) for x in val.split() )
|
|
||||||
return set(val.split())
|
|
||||||
if hint is dict or get_origin(hint) is dict:
|
|
||||||
return json.loads(val)
|
|
||||||
if hint is Union or get_origin(hint) is Union:
|
|
||||||
# TODO str will never fail, should be tried last.
|
|
||||||
# cheap fix: sort keys by name so that "str" comes last
|
|
||||||
for t in sorted(get_args(hint), key=lambda x : str(x)):
|
|
||||||
try:
|
|
||||||
return t(val)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
return (get_origin(hint) or hint)(val) # try to instantiate directly
|
|
||||||
|
|
||||||
class ConfigObject:
|
|
||||||
def __getitem__(self, key: str) -> Any:
|
|
||||||
return getattr(self, key)
|
|
||||||
|
|
||||||
class Addon:
|
|
||||||
name: str
|
|
||||||
config: ConfigObject
|
|
||||||
logger: logging.Logger
|
|
||||||
|
|
||||||
_client: 'Treepuncher'
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class Options(ConfigObject):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self) -> 'Treepuncher':
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
def __init__(self, client: 'Treepuncher', *args, **kwargs):
|
|
||||||
self._client = client
|
|
||||||
self.name = type(self).__name__
|
|
||||||
cfg = self._client.config
|
|
||||||
opts: Dict[str, Any] = {}
|
|
||||||
cfg_clazz = get_type_hints(type(self))['config']
|
|
||||||
if cfg_clazz is not ConfigObject:
|
|
||||||
for field in fields(cfg_clazz):
|
|
||||||
default = field.default if field.default is not MISSING \
|
|
||||||
else field.default_factory() if field.default_factory is not MISSING \
|
|
||||||
else MISSING
|
|
||||||
if cfg.has_option(self.name, field.name):
|
|
||||||
opts[field.name] = parse_with_hint(self._client.config[self.name].get(field.name), field.type)
|
|
||||||
elif default is MISSING:
|
|
||||||
repr_type = field.type.__name__ if isinstance(field.type, type) else str(field.type) # TODO fix for 3.8 I think?
|
|
||||||
raise ValueError(
|
|
||||||
f"Missing required value '{field.name}' of type '{repr_type}' in section '{self.name}'"
|
|
||||||
)
|
|
||||||
else: # not really necessary since it's a dataclass but whatever
|
|
||||||
opts[field.name] = default
|
|
||||||
self.config = self.Options(**opts)
|
|
||||||
self.logger = self._client.logger.getChild(self.name)
|
|
||||||
self.register()
|
|
||||||
|
|
||||||
def register(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def cleanup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Notifier(Addon):
|
|
||||||
_report_functions : List[Callable]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self._report_functions = []
|
|
||||||
|
|
||||||
def add_reporter(self, fn:Callable):
|
|
||||||
self._report_functions.append(fn)
|
|
||||||
return fn
|
|
||||||
|
|
||||||
def report(self) -> str:
|
|
||||||
return '\n'.join(str(fn()).strip() for fn in self._report_functions)
|
|
||||||
|
|
||||||
def notify(self, text, log:bool = False, **kwargs):
|
|
||||||
print(text)
|
|
||||||
|
|
||||||
async def initialize(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def cleanup(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class MissingParameterError(Exception):
|
class MissingParameterError(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -137,7 +37,7 @@ class Treepuncher(
|
||||||
config: ConfigParser
|
config: ConfigParser
|
||||||
storage: Storage
|
storage: Storage
|
||||||
|
|
||||||
notifier: Optional[Notifier]
|
notifier: Notifier
|
||||||
scheduler: AsyncIOScheduler
|
scheduler: AsyncIOScheduler
|
||||||
modules: List[Addon]
|
modules: List[Addon]
|
||||||
ctx: Dict[Any, Any]
|
ctx: Dict[Any, Any]
|
||||||
|
@ -186,8 +86,9 @@ class Treepuncher(
|
||||||
|
|
||||||
self.storage = Storage(self.name)
|
self.storage = Storage(self.name)
|
||||||
|
|
||||||
|
self.notifier = Notifier(self)
|
||||||
|
|
||||||
self.modules = []
|
self.modules = []
|
||||||
self.notifier = None
|
|
||||||
|
|
||||||
# tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # This doesn't work anymore
|
# tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # This doesn't work anymore
|
||||||
self.scheduler = AsyncIOScheduler() # TODO APScheduler warns about timezone ugghh
|
self.scheduler = AsyncIOScheduler() # TODO APScheduler warns about timezone ugghh
|
||||||
|
@ -233,12 +134,11 @@ class Treepuncher(
|
||||||
# if self.started: # TODO readd check
|
# if self.started: # TODO readd check
|
||||||
# return
|
# return
|
||||||
await super().start()
|
await super().start()
|
||||||
if not self.notifier:
|
|
||||||
self.notifier = Notifier(self)
|
await self.notifier.start()
|
||||||
await self.notifier.initialize()
|
await asyncio.gather(
|
||||||
for m in self.modules:
|
*(m.initialize() for m in self.modules)
|
||||||
if not isinstance(m, Notifier):
|
)
|
||||||
await m.initialize()
|
|
||||||
self._processing = True
|
self._processing = True
|
||||||
self._worker = asyncio.get_event_loop().create_task(self._work())
|
self._worker = asyncio.get_event_loop().create_task(self._work())
|
||||||
self.scheduler.resume()
|
self.scheduler.resume()
|
||||||
|
@ -253,18 +153,20 @@ class Treepuncher(
|
||||||
if not force:
|
if not force:
|
||||||
await self._worker
|
await self._worker
|
||||||
await self.join_callbacks()
|
await self.join_callbacks()
|
||||||
for m in self.modules:
|
await asyncio.gather(
|
||||||
await m.cleanup()
|
*(m.cleanup() for m in self.modules)
|
||||||
|
)
|
||||||
await super().stop()
|
await super().stop()
|
||||||
self.logger.info("Treepuncher stopped")
|
self.logger.info("Treepuncher stopped")
|
||||||
|
|
||||||
def install(self, module: Type[Addon]) -> Type[Addon]:
|
def install(self, module: Type[Addon]) -> Type[Addon]:
|
||||||
m = module(self)
|
m = module(self)
|
||||||
self.modules.append(m)
|
if isinstance(m, Provider):
|
||||||
if isinstance(m, Notifier):
|
self.notifier.add_provider(m)
|
||||||
if self.notifier:
|
elif isinstance(m, Addon):
|
||||||
self.logger.warning("Replacing previously loaded notifier %s", str(self.notifier))
|
self.modules.append(m)
|
||||||
self.notifier = m
|
else:
|
||||||
|
raise ValueError("Given type is not an addon")
|
||||||
return module
|
return module
|
||||||
|
|
||||||
async def _work(self):
|
async def _work(self):
|
||||||
|
|
Loading…
Reference in a new issue