diff --git a/appdaemon/__main__.py b/appdaemon/__main__.py index 1ab58ddcb..2a7be42fa 100644 --- a/appdaemon/__main__.py +++ b/appdaemon/__main__.py @@ -15,13 +15,13 @@ import signal import sys -import appdaemon.appdaemon as ad -import appdaemon.http as adhttp -import appdaemon.logging as logging -import appdaemon.utils as utils import pytz +import appdaemon.appdaemon as ad +import appdaemon.utils as utils from appdaemon.app_management import UpdateMode +from appdaemon.http import HTTP +from appdaemon.logging import Logging try: import pid @@ -39,6 +39,8 @@ class ADMain: Class to encapsulate all main() functionality. """ + logging: Logging + def __init__(self): """Constructor.""" @@ -104,7 +106,7 @@ def stop(self): self.http_object.stop() # noinspection PyBroadException,PyBroadException - def run(self, appdaemon, hadashboard, admin, aui, api, http): + def run(self, appdaemon: ad.AppDaemon, hadashboard, admin, aui, api, http): """Start AppDaemon up after initial argument parsing. Args: @@ -126,7 +128,7 @@ def run(self, appdaemon, hadashboard, admin, aui, api, http): self.logger.info("Running AD using uvloop") uvloop.install() - loop = asyncio.get_event_loop() + loop: asyncio.BaseEventLoop = asyncio.get_event_loop() # Initialize AppDaemon @@ -138,7 +140,7 @@ def run(self, appdaemon, hadashboard, admin, aui, api, http): hadashboard is not None or admin is not None or aui is not None or api is not False ): self.logger.info("Initializing HTTP") - self.http_object = adhttp.HTTP( + self.http_object = HTTP( self.AD, loop, self.logging, @@ -357,7 +359,7 @@ def main(self): # noqa: C901 else: logs = {} - self.logging = logging.Logging(logs, args.debug) + self.logging = Logging(logs, args.debug) self.logger = self.logging.get_logger() if "time_zone" in config["appdaemon"]: diff --git a/appdaemon/adapi.py b/appdaemon/adapi.py index 09a385ba8..14f809013 100644 --- a/appdaemon/adapi.py +++ b/appdaemon/adapi.py @@ -1,18 +1,19 @@ -from datetime import timedelta -from copy import deepcopy -from typing import Any, Optional, Callable, Union -from asyncio import Future - import asyncio import datetime as dt import inspect import re import uuid +from asyncio import Future +from copy import deepcopy +from datetime import timedelta +from typing import Any, Callable, Dict, Optional, Union + import iso8601 from appdaemon import utils from appdaemon.appdaemon import AppDaemon from appdaemon.entity import Entity +from appdaemon.logging import Logging class ADAPI: @@ -22,10 +23,32 @@ class ADAPI: """ + AD: AppDaemon + """Reference to the top-level AppDaemon container object + """ + name: str + """The app name, which is set by the top-level key in the YAML file + """ + _logging: Logging + """Reference to the Logging subsystem object + """ + args: Dict[str, Any] + """The arguments provided in this app's YAML config file + """ + # # Internal parameters # - def __init__(self, ad: AppDaemon, name, logging_obj, args, config, app_config, global_vars): + def __init__( + self, + ad: AppDaemon, + name: str, + logging_obj: Logging, + args: Dict[str, Any], + config: Dict[str, Any], + app_config, + global_vars, + ): # Store args self.AD = ad @@ -33,6 +56,7 @@ def __init__(self, ad: AppDaemon, name, logging_obj, args, config, app_config, g self._logging = logging_obj self.config = config self.app_config = app_config + # same as self.AD.app_management.app_config self.args = deepcopy(args) self.app_dir = self.AD.app_dir self.config_dir = self.AD.config_dir @@ -58,7 +82,7 @@ def __init__(self, ad: AppDaemon, name, logging_obj, args, config, app_config, g @staticmethod def _sub_stack(msg): # If msg is a data structure of some type, don't sub - if type(msg) is str: + if isinstance(msg, str): stack = inspect.stack() if msg.find("__module__") != -1: msg = msg.replace("__module__", stack[2][1]) @@ -2494,9 +2518,9 @@ async def run_once(self, callback: Callable, start: Union[dt.time, str], **kwarg >>> handle = self.run_once(self.run_once_c, "sunrise + 01:00:00") """ - if type(start) == dt.time: + if isinstance(start, dt.time): when = start - elif type(start) == str: + elif isinstance(start, str): start_time_obj = await self.AD.sched._parse_time(start, self.name) when = start_time_obj["datetime"].time() else: @@ -2571,9 +2595,9 @@ async def run_at(self, callback: Callable, start: Union[dt.datetime, str], **kwa >>> handle = self.run_at(self.run_at_c, "sunrise + 01:00:00") """ - if type(start) == dt.datetime: + if isinstance(start, dt.datetime): when = start - elif type(start) == str: + elif isinstance(start, str): start_time_obj = await self.AD.sched._parse_time(start, self.name) when = start_time_obj["datetime"] else: @@ -2644,9 +2668,9 @@ async def run_daily(self, callback: Callable, start: Union[dt.time, str], **kwar """ info = None when = None - if type(start) == dt.time: + if isinstance(start, dt.time): when = start - elif type(start) == str: + elif isinstance(start, str): info = await self.AD.sched._parse_time(start, self.name) else: raise ValueError("Invalid type for start") diff --git a/appdaemon/admin.py b/appdaemon/admin.py index ee9abaf1f..bc8b71b32 100644 --- a/appdaemon/admin.py +++ b/appdaemon/admin.py @@ -1,14 +1,17 @@ import os import traceback +from typing import TYPE_CHECKING from jinja2 import Environment, FileSystemLoader, select_autoescape import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Admin: - def __init__(self, config_dir, logger, ad: AppDaemon, **kwargs): + def __init__(self, config_dir, logger, ad: "AppDaemon", **kwargs): # # Set Defaults # diff --git a/appdaemon/admin_loop.py b/appdaemon/admin_loop.py index c79d1341d..3acfae44e 100644 --- a/appdaemon/admin_loop.py +++ b/appdaemon/admin_loop.py @@ -1,9 +1,23 @@ import asyncio -from appdaemon.appdaemon import AppDaemon +from logging import Logger +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class AdminLoop: - def __init__(self, ad: AppDaemon): + """Called by :meth:`~appdaemon.appdaemon.AppDaemon.register_http`. Loop timed with :attr:`~appdaemon.AppDaemon.admin_delay`""" + + AD: "AppDaemon" + """Reference to the AppDaemon container object + """ + stopping: bool + logger: Logger + """Standard python logger named ``AppDaemon._admin_loop`` + """ + + def __init__(self, ad: "AppDaemon"): self.AD = ad self.stopping = False self.logger = ad.logging.get_child("_admin_loop") @@ -13,6 +27,7 @@ def stop(self): self.stopping = True async def loop(self): + """Handles calling :meth:`~.threading.Threading.get_callback_update` and :meth:`~.threading.Threading.get_q_update`""" while not self.stopping: if self.AD.http.stats_update != "none" and self.AD.sched is not None: await self.AD.threading.get_callback_update() diff --git a/appdaemon/app_management.py b/appdaemon/app_management.py index 9a3364324..4cbba326f 100644 --- a/appdaemon/app_management.py +++ b/appdaemon/app_management.py @@ -9,16 +9,19 @@ import subprocess import sys import traceback -from types import ModuleType import uuid from collections import OrderedDict from dataclasses import dataclass, field from enum import Enum +from logging import Logger from pathlib import Path -from typing import Dict, Iterable, List, Literal, Union, Optional +from types import ModuleType +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Union import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class UpdateMode(Enum): @@ -52,7 +55,7 @@ class ModuleLoad: name: str = field(init=False, repr=True) def __post_init__(self): - self.path = Path(self.path) + self.path = Path(self.path).resolve() if self.path.name == "__init__.py": self.name = self.path.parent.name @@ -84,24 +87,45 @@ def mark_app_for_termination(self, appname: str): class AppManagement: - """Subsystem container for managing app lifecycles + """Subsystem container for managing app lifecycles""" - Attributes: - monitored_files: Dictionary of the Python files that are being watched for changes and their last modified times - filter_files: Dictionary of the modified times of the filter files and their paths. - modules: Dictionary of the loaded modules and their names - objects: Dictionary of dictionaries with the instantiated apps, plugins, and sequences along with some metadata + AD: "AppDaemon" + """Reference to the top-level AppDaemon container object """ - - AD: AppDaemon use_toml: bool + """Whether to use TOML files for configuration + """ ext: Literal[".yaml", ".toml"] + logger: Logger + """Standard python logger named ``AppDaemon._app_management`` + """ + error: Logger + """Standard python logger named ``Error`` + """ monitored_files: Dict[Union[str, Path], float] + """Dictionary of the Python files that are being watched for changes and their last modified times + """ filter_files: Dict[str, float] + """Dictionary of the modified times of the filter files and their paths. + """ modules: Dict[str, ModuleType] - objects: Dict[str, Dict] + """Dictionary of the loaded modules and their names + """ + objects: Dict[str, Dict[str, Any]] + """Dictionary of dictionaries with the instantiated apps, plugins, and sequences along with some metadata. Gets populated by - def __init__(self, ad: AppDaemon, use_toml: bool): + - ``self.init_object``, which instantiates the app classes + - ``self.init_plugin_object`` + - ``self.init_sequence_object`` + """ + app_config: Dict[str, Dict[str, Dict[str, bool]]] + """Keeps track of which module and class each app comes from, along with any associated global modules. Gets set at the end of :meth:`~appdaemon.app_management.AppManagement.check_config`. + """ + active_apps: List[str] + inactive_apps: List[str] + non_apps: List[str] + + def __init__(self, ad: "AppDaemon", use_toml: bool): self.AD = ad self.use_toml = use_toml self.ext = ".toml" if use_toml is True else ".yaml" @@ -122,7 +146,7 @@ def __init__(self, ad: AppDaemon, use_toml: bool): self.module_dirs = [] # Keeps track of the name of the module and class to load for each app name - self.app_config: Dict[str, Dict[str, Dict[str, bool]]] = {} + self.app_config = {} self.global_module_dependencies = {} self.apps_initialized = False @@ -217,7 +241,7 @@ async def get_app_instance(self, name: str, id): if name in self.objects and self.objects[name]["id"] == id: return self.AD.app_management.objects[name]["object"] - async def initialize_app(self, name): + async def initialize_app(self, name: str): if name in self.objects: init = getattr(self.objects[name]["object"], "initialize", None) if init is None: @@ -372,7 +396,12 @@ def get_app_debug_level(self, app): else: return "None" - async def init_object(self, app_name): + async def init_object(self, app_name: str): + """Instantiates an app by name and stores it in ``self.objects`` + + Args: + app_name (str): Name of the app, as defined in a config file + """ app_args = self.app_config[app_name] # as it appears in the YAML definition of the app @@ -481,7 +510,12 @@ async def terminate_sequence(self, name: str) -> bool: return True - async def read_config(self): # noqa: C901 + async def read_config(self) -> Dict[str, Dict[str, Any]]: # noqa: C901 + """Walks the apps directory and reads all the config files with :func:`~.utils.read_config_file`, which reads individual config files and runs in the :attr:`~.appdaemon.AppDaemon.executor`. + + Returns: + Dict[str, Dict[str, Any]]: Loaded app configuration + """ new_config = None for root, subdirs, files in await utils.run_in_executor(self, os.walk, self.AD.app_dir): @@ -491,7 +525,7 @@ async def read_config(self): # noqa: C901 if file[-5:] == self.ext and file[0] != ".": path = os.path.join(root, file) self.logger.debug("Reading %s", path) - config = await utils.run_in_executor(self, self.read_config_file, path) + config: Dict[str, Dict] = await utils.run_in_executor(self, self.read_config_file, path) valid_apps = {} if type(config).__name__ == "dict": for app in config: @@ -658,7 +692,8 @@ def check_later_app_configs(self, last_latest): return later_files # Run in executor - def read_config_file(self, file): + def read_config_file(self, file) -> Dict[str, Dict]: + """Reads a single YAML or TOML file.""" try: return utils.read_config_file(file) except Exception: @@ -669,7 +704,16 @@ def read_config_file(self, file): self.logger.warning("-" * 60) # noinspection PyBroadException - async def check_config(self, silent=False, add_threads=True) -> Optional[AppActions]: # noqa: C901 + async def check_config(self, silent: bool = False, add_threads: bool = True) -> Optional[AppActions]: # noqa: C901 + """Wraps :meth:`~AppManagement.read_config` + + Args: + silent (bool, optional): _description_. Defaults to False. + add_threads (bool, optional): _description_. Defaults to True. + + Returns: + AppActions object with information about which apps to initialize and/or terminate + """ terminate_apps = {} initialize_apps = {} total_apps = len(self.app_config) @@ -823,7 +867,7 @@ def get_app_from_file(self, file): """Finds the apps that depend on a given file""" module_name = self.get_module_from_path(file) for app_name, cfg in self.app_config.items(): - if "module" in cfg and cfg["module"] == module_name: + if "module" in cfg and cfg["module"].startswith(module_name): return app_name return None @@ -866,7 +910,6 @@ def read_app(self, reload_cfg: ModuleLoad): elif "global_modules" in self.app_config and module_name in self.app_config["global_modules"]: self.logger.info("Loading Global Module: %s", module_name) self.modules[module_name] = importlib.import_module(module_name) - # elif "global" in else: if self.AD.missing_app_warnings: self.logger.warning("No app description found for: %s - ignoring", module_name) @@ -892,8 +935,8 @@ def get_file_from_module(self, module_name: str) -> Optional[Path]: return None else: module_path = Path(module_obj.__file__) - if all(isinstance(f, Path) for f in self.monitored_files): - assert module_path in self.monitored_files + if self.monitored_files and all(isinstance(f, Path) for f in self.monitored_files): + assert module_path in self.monitored_files, f"{module_path} is not being monitored" return module_path def get_path_from_app(self, app_name: str) -> Path: @@ -1057,7 +1100,7 @@ async def _init_update_mode(self): def get_python_files(self) -> List[Path]: return [ f - for f in Path(self.AD.app_dir).rglob("*.py") + for f in Path(self.AD.app_dir).resolve().rglob("*.py") # Prune dir list if f.parent.name not in self.AD.exclude_dirs and "." not in f.parent.name ] @@ -1729,7 +1772,7 @@ async def manage_services(self, namespace, domain, service, kwargs): return None - async def increase_active_apps(self, name): + async def increase_active_apps(self, name: str): if name not in self.active_apps: self.active_apps.append(name) @@ -1742,7 +1785,7 @@ async def increase_active_apps(self, name): await self.set_state(self.active_apps_sensor, state=active_apps) await self.set_state(self.inactive_apps_sensor, state=inactive_apps) - async def increase_inactive_apps(self, name): + async def increase_inactive_apps(self, name: str): if name not in self.inactive_apps: self.inactive_apps.append(name) diff --git a/appdaemon/appdaemon.py b/appdaemon/appdaemon.py index 0a902e33f..c6659c94b 100755 --- a/appdaemon/appdaemon.py +++ b/appdaemon/appdaemon.py @@ -1,12 +1,38 @@ -import concurrent.futures import os import os.path import threading +from asyncio import BaseEventLoop +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +from typing import TYPE_CHECKING, Union + +import appdaemon.utils as utils +from appdaemon.admin_loop import AdminLoop +from appdaemon.app_management import AppManagement +from appdaemon.callbacks import Callbacks +from appdaemon.events import Events +from appdaemon.futures import Futures +from appdaemon.plugin_management import Plugins +from appdaemon.scheduler import Scheduler +from appdaemon.sequences import Sequences +from appdaemon.services import Services +from appdaemon.state import State +from appdaemon.thread_async import ThreadAsync +from appdaemon.threading import Threading +from appdaemon.utility_loop import Utility + +if TYPE_CHECKING: + from appdaemon.http import HTTP + from appdaemon.logging import Logging class AppDaemon: """Top-level container for the subsystem objects. This gets passed to the subsystem objects and stored in them as the ``self.AD`` attribute. + Asyncio: + + :class:`~concurrent.futures.ThreadPoolExecutor` + Subsystems: .. list-table:: @@ -15,50 +41,75 @@ class AppDaemon: * - Attribute - Object + * - ``app_management`` + - :class:`~.app_management.AppManagement` + * - ``callbacks`` + - :class:`~.callbacks.Callbacks` + * - ``events`` + - :class:`~.events.Events` + * - ``futures`` + - :class:`~.futures.Futures` + * - ``http`` + - :class:`~.http.HTTP` + * - ``plugins`` + - :class:`~.plugin_management.Plugins` + * - ``scheduler`` + - :class:`~.scheduler.Scheduler` * - ``services`` - :class:`~.services.Services` * - ``sequences`` - :class:`~.sequences.Sequences` * - ``state`` - :class:`~.state.State` - * - ``events`` - - :class:`~.events.Events` - * - ``callbacks`` - - :class:`~.callbacks.Callbacks` - * - ``futures`` - - :class:`~.futures.Futures` - * - ``app_management`` - - :class:`~.app_management.AppManagement` * - ``threading`` - :class:`~.threading.Threading` - * - ``executor`` - - :class:`~concurrent.futures.ThreadPoolExecutor` - * - ``plugins`` - - :class:`~.plugin_management.Plugins` * - ``utility`` - :class:`~.utility_loop.Utility` + """ - def __init__(self, logging, loop, **kwargs): - # - # Import various AppDaemon bits and pieces now to avoid circular import - # + # asyncio + loop: BaseEventLoop + """Main asyncio event loop + """ + executor: ThreadPoolExecutor + """Executes functions from a pool of async threads. Configured with the ``threadpool_workers`` key. Defaults to 10. + """ - import appdaemon.app_management as apps - import appdaemon.callbacks as callbacks - import appdaemon.events as events - import appdaemon.futures as futures - import appdaemon.plugin_management as plugins - import appdaemon.scheduler as scheduler - import appdaemon.sequences as sequences - import appdaemon.services as services - import appdaemon.state as state - import appdaemon.thread_async as appq - import appdaemon.threading - import appdaemon.utility_loop as utility - import appdaemon.utils as utils + # subsystems + app_management: AppManagement + callbacks: Callbacks + events: Events + futures: Futures + http: "HTTP" + logging: "Logging" + plugins: Plugins + scheduler: Scheduler + services: Services + sequences: Sequences + state: State + threading: Threading + utility: Utility + + # shut down flag + stopping: bool + + # settings + app_dir: Union[str, Path] + """Defined in the main YAML config under ``appdaemon.app_dir``. Defaults to ``./apps`` + """ + config_dir: Union[str, Path] + """Path to the AppDaemon configuration files. Defaults to the first folder that has ``./apps`` + - ``~/.homeassistant`` + - ``/etc/appdaemon`` + """ + apps: bool + """Flag for whether ``disable_apps`` was set in the AppDaemon config + """ + + def __init__(self, logging: "Logging", loop: BaseEventLoop, **kwargs): self.logging = logging self.logging.register_ad(self) self.logger = logging.get_logger() @@ -72,10 +123,6 @@ def __init__(self, logging, loop, **kwargs): self.config["ad_version"] = utils.__version__ self.check_app_updates_profile = "" - self.was_dst = False - - self.last_state = None - self.executor = None self.loop = None self.srv = None @@ -223,37 +270,37 @@ def __init__(self, logging, loop, **kwargs): # # Set up services # - self.services = services.Services(self) + self.services = Services(self) # # Set up sequences # - self.sequences = sequences.Sequences(self) + self.sequences = Sequences(self) # # Set up scheduler # - self.sched = scheduler.Scheduler(self) + self.sched = Scheduler(self) # # Set up state # - self.state = state.State(self) + self.state = State(self) # # Set up events # - self.events = events.Events(self) + self.events = Events(self) # # Set up callbacks # - self.callbacks = callbacks.Callbacks(self) + self.callbacks = Callbacks(self) # # Set up futures # - self.futures = futures.Futures(self) + self.futures = Futures(self) if self.apps is True: if self.app_dir is None: @@ -266,13 +313,16 @@ def __init__(self, logging, loop, **kwargs): utils.check_path("config_dir", self.logger, self.config_dir, permissions="rwx") utils.check_path("appdir", self.logger, self.app_dir) + self.config_dir = os.path.abspath(self.config_dir) + self.app_dir = os.path.abspath(self.app_dir) + # Initialize Apps - self.app_management = apps.AppManagement(self, self.use_toml) + self.app_management = AppManagement(self, self.use_toml) # threading setup - self.threading = appdaemon.threading.Threading(self, kwargs) + self.threading = Threading(self, kwargs) self.stopping = False @@ -282,33 +332,34 @@ def __init__(self, logging, loop, **kwargs): if "threadpool_workers" in kwargs: self.threadpool_workers = int(kwargs["threadpool_workers"]) - self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=self.threadpool_workers) + self.executor = ThreadPoolExecutor(max_workers=self.threadpool_workers) # Initialize Plugins - - if "plugins" in kwargs: - args = kwargs["plugins"] - else: - args = None - - self.plugins = plugins.Plugins(self, args) + args = kwargs.get("plugins", None) + self.plugins = Plugins(self, args) # Create thread_async Loop - self.logger.debug("Starting thread_async loop") - if self.apps is True: - self.thread_async = appq.ThreadAsync(self) + self.thread_async = ThreadAsync(self) loop.create_task(self.thread_async.loop()) # Create utility loop - self.logger.debug("Starting utility loop") - - self.utility = utility.Utility(self) + self.utility = Utility(self) loop.create_task(self.utility.loop()) def stop(self): + """Called by the signal handler to shut AD down. + + Also stops + + - :class:`~.admin_loop.AdminLoop` + - :class:`~.thread_async.ThreadAsync` + - :class:`~.scheduler.Scheduler` + - :class:`~.utility_loop.Utility` + - :class:`~.plugin_management.Plugins` + """ self.stopping = True if self.admin_loop is not None: self.admin_loop.stop() @@ -329,14 +380,14 @@ def terminate(self): # Utilities # - def register_http(self, http): - import appdaemon.admin_loop as admin_loop + def register_http(self, http: "HTTP"): + """Sets the ``self.http`` attribute with a :class:`~.http.HTTP` object and starts the admin loop.""" - self.http = http + self.http: "HTTP" = http # Create admin loop if http.old_admin is not None or http.admin is not None: self.logger.debug("Starting admin loop") - self.admin_loop = admin_loop.AdminLoop(self) + self.admin_loop = AdminLoop(self) self.loop.create_task(self.admin_loop.loop()) diff --git a/appdaemon/callbacks.py b/appdaemon/callbacks.py index db5068e07..9e65b39f4 100644 --- a/appdaemon/callbacks.py +++ b/appdaemon/callbacks.py @@ -1,19 +1,28 @@ import asyncio +from logging import Logger +from typing import TYPE_CHECKING import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Callbacks: - """Subsystem container for events + """Container for storing callbacks. Modified by :class:`~.events.Events` and :class:`~.state.State`""" - Attributes: - AD: Reference to the AppDaemon container object + AD: "AppDaemon" + """Reference to the AppDaemon container object + """ + logger: Logger + """Standard python logger named ``AppDaemon._callbacks`` + """ + diag: Logger + """Standard python logger named ``Diag`` """ - def __init__(self, ad: AppDaemon): + def __init__(self, ad: "AppDaemon"): self.AD = ad - self.callbacks = {} self.callbacks_lock = asyncio.Lock() self.logger = ad.logging.get_child("_callbacks") @@ -24,6 +33,7 @@ def __init__(self, ad: AppDaemon): # async def dump_callbacks(self): + """Dumps info about the callbacks to the ``Diag`` log""" async with self.callbacks_lock: if self.callbacks == {}: self.diag.info("No callbacks") diff --git a/appdaemon/dashboard.py b/appdaemon/dashboard.py index 5cce11bac..4dc65a7c0 100755 --- a/appdaemon/dashboard.py +++ b/appdaemon/dashboard.py @@ -1,17 +1,18 @@ -import os import ast -import re -import yaml -from jinja2 import Environment, BaseLoader, FileSystemLoader, select_autoescape -import traceback -import functools -import time import cProfile +import datetime +import functools import io +import os import pstats -import datetime +import re +import time +import traceback from collections import OrderedDict +import yaml +from jinja2 import BaseLoader, Environment, FileSystemLoader, select_autoescape + import appdaemon.utils as ha @@ -162,7 +163,7 @@ def _resolve_css_params(self, fields, subs): for varline in fields: if isinstance(fields[varline], dict): fields[varline] = self._resolve_css_params(fields[varline], subs) - elif fields[varline] is not None and type(fields[varline]) == str: + elif fields[varline] is not None and isinstance(fields[varline], str): _vars = variable.finditer(fields[varline]) for var in _vars: subvar = var.group()[1:] diff --git a/appdaemon/entity.py b/appdaemon/entity.py index d9aefdabc..4aad22c68 100644 --- a/appdaemon/entity.py +++ b/appdaemon/entity.py @@ -1,13 +1,16 @@ -from appdaemon.appdaemon import AppDaemon -from appdaemon.exceptions import TimeOutException -import appdaemon.utils as utils - -from typing import Any, Optional, Callable, Union -from logging import Logger import asyncio import uuid -import iso8601 from collections.abc import Iterable +from logging import Logger +from typing import TYPE_CHECKING, Any, Callable, Optional, Union + +import iso8601 + +import appdaemon.utils as utils +from appdaemon.exceptions import TimeOutException + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class EntityAttrs: @@ -20,11 +23,12 @@ def __get__(self, instance, owner): class Entity: + AD: "AppDaemon" + name: str + logger: Logger states_attrs = EntityAttrs() - def __init__(self, logger: Logger, ad: AppDaemon, name: str, namespace: str, entity_id: str): - # Store args - + def __init__(self, logger: Logger, ad: "AppDaemon", name: str, namespace: str, entity_id: str): self.AD = ad self.name = name self.logger = logger @@ -434,7 +438,7 @@ async def entity_state_changed(self, *args: list, **kwargs: dict) -> None: # @classmethod - def entity_api(cls, logger: Logger, ad: AppDaemon, name: str, namespace: str, entity: str): + def entity_api(cls, logger: Logger, ad: "AppDaemon", name: str, namespace: str, entity: str): return cls(logger, ad, name, namespace, entity) # diff --git a/appdaemon/events.py b/appdaemon/events.py index fe2ea55a3..a1376432d 100644 --- a/appdaemon/events.py +++ b/appdaemon/events.py @@ -1,33 +1,29 @@ +import datetime +import traceback import uuid from copy import deepcopy -import traceback -import datetime +from logging import Logger +from typing import TYPE_CHECKING, Any, Dict -from appdaemon.appdaemon import AppDaemon import appdaemon.utils as utils +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon + class Events: - """Subsystem container for handling all events + """Subsystem container for handling all events""" - Attributes: - AD: Reference to the AppDaemon container object + AD: "AppDaemon" + """Reference to the top-level AppDaemon container object + """ + logger: Logger + """Standard python logger named ``AppDaemon._events`` """ - AD: AppDaemon - - def __init__(self, ad: AppDaemon): - """Constructor. - - Args: - ad: Reference to the AppDaemon object - """ - + def __init__(self, ad: "AppDaemon"): self.AD = ad self.logger = ad.logging.get_child("_events") - # - # Events - # async def add_event_callback(self, name, namespace, cb, event, **kwargs): """Adds a callback for an event which is called internally by apps. @@ -134,12 +130,15 @@ async def cancel_event_callback(self, name, handle): return executed - async def info_event_callback(self, name, handle): + async def info_event_callback(self, name: str, handle: str): """Gets the information of an event callback. Args: name (str): Name of the app or subsystem. - handle: Previously supplied handle for the callback. + handle (str): Previously supplied handle for the callback. + + Raises: + ValueError: an invalid name or handle was provided Returns: A dictionary of callback entries or rise a ``ValueError`` if an invalid handle is provided. @@ -153,7 +152,7 @@ async def info_event_callback(self, name, handle): else: raise ValueError("Invalid handle: {}".format(handle)) - async def fire_event(self, namespace, event, **kwargs): + async def fire_event(self, namespace: str, event: str, **kwargs): """Fires an event. If the namespace does not have a plugin associated with it, the event will be fired locally. @@ -181,7 +180,7 @@ async def fire_event(self, namespace, event, **kwargs): # Just fire the event locally await self.AD.events.process_event(namespace, {"event_type": event, "data": kwargs}) - async def process_event(self, namespace, data): + async def process_event(self, namespace: str, data: Dict[str, Any]): """Processes an event that has been received either locally or from a plugin. Args: @@ -261,7 +260,7 @@ async def process_event(self, namespace, data): self.logger.warning(traceback.format_exc()) self.logger.warning("-" * 60) - async def has_log_callback(self, name): + async def has_log_callback(self, name: str): """Returns ``True`` if the app has a log callback, ``False`` otherwise. Used to prevent callback loops. In the calling logic, if this function returns diff --git a/appdaemon/futures.py b/appdaemon/futures.py index f98c88e06..145746b37 100644 --- a/appdaemon/futures.py +++ b/appdaemon/futures.py @@ -1,9 +1,20 @@ -from appdaemon.appdaemon import AppDaemon import functools +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Futures: - def __init__(self, ad: AppDaemon): + """Subsystem container for managing :class:`~asyncio.Future` objects + + Attributes: + AD: Reference to the AppDaemon container object + """ + + AD: "AppDaemon" + + def __init__(self, ad: "AppDaemon"): self.AD = ad self.futures = {} diff --git a/appdaemon/http.py b/appdaemon/http.py index bd5c11edc..db9d4ccd0 100644 --- a/appdaemon/http.py +++ b/appdaemon/http.py @@ -1,25 +1,27 @@ import asyncio +import concurrent.futures import json import os import re +import ssl import time import traceback -import concurrent.futures -from typing import Callable, Optional +import uuid +from typing import TYPE_CHECKING, Callable, Optional from urllib.parse import urlparse + +import bcrypt import feedparser from aiohttp import web -import ssl -import bcrypt -import uuid from jinja2 import Environment, FileSystemLoader, select_autoescape +import appdaemon.admin as adadmin import appdaemon.dashboard as addashboard -import appdaemon.utils as utils import appdaemon.stream.adstream as stream -import appdaemon.admin as adadmin +import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon def securedata(myfunc): @@ -107,7 +109,16 @@ async def wrapper(*args): class HTTP: - def __init__(self, ad: AppDaemon, loop, logging, appdaemon, dashboard, old_admin, admin, api, http): + """Handles serving the web UI""" + + AD: "AppDaemon" + """Reference to the AppDaemon container object + """ + + stopping: bool + executor: concurrent.futures.ThreadPoolExecutor + + def __init__(self, ad: "AppDaemon", loop, logging, appdaemon, dashboard, old_admin, admin, api, http): self.AD = ad self.logging = logging self.logger = ad.logging.get_child("_http") diff --git a/appdaemon/logging.py b/appdaemon/logging.py index 28f7081f3..de029c38f 100644 --- a/appdaemon/logging.py +++ b/appdaemon/logging.py @@ -1,21 +1,31 @@ +import copy import datetime -import pytz +import logging import sys +import traceback import uuid -import copy - -import logging -from logging.handlers import RotatingFileHandler -from logging import StreamHandler from collections import OrderedDict -import traceback +from logging import Logger, StreamHandler +from logging.handlers import RotatingFileHandler +from typing import Any, Callable, Dict, List, Optional, Union + +import pytz -from appdaemon.thread_async import AppDaemon import appdaemon.utils as utils +from appdaemon.appdaemon import AppDaemon class DuplicateFilter(logging.Filter): - def __init__(self, logger, threshold, delay, timeout): + """:class:`logging.Filter` that filters duplicate messages""" + + threshold: int + timeout: float + delay: float + filtering: bool + """Flag to track if the filter is active or not. + """ + + def __init__(self, logger: logging.Logger, threshold: float, delay: float, timeout: float): self.logger = logger self.last_log = None self.current_count = 0 @@ -27,7 +37,7 @@ def __init__(self, logger, threshold, delay, timeout): self.timeout = timeout self.last_log_time = None - def filter(self, record): + def filter(self, record: logging.LogRecord) -> bool: if record.msg == "Previous message repeated %s times": return True if self.threshold == 0: @@ -165,6 +175,14 @@ def emit(self, record): class Logging: + """Creates and configures the Python logging. The top-level logger is called ``AppDaemon``. Child loggers are created with :meth:`~Logging.get_child`.""" + + AD: "AppDaemon" + """Reference to the top-level AppDaemon container object + """ + + config: Dict[str, Dict[str, Any]] + log_levels = { "CRITICAL": 50, "ERROR": 40, @@ -174,7 +192,7 @@ class Logging: "NOTSET": 0, } - def __init__(self, config, log_level): + def __init__(self, config: Optional[Dict] = None, log_level: str = "INFO"): self.AD = None self.tz = None @@ -367,7 +385,8 @@ def separate_error_log(self): return True return False - def register_ad(self, ad): + def register_ad(self, ad: "AppDaemon"): + """Adds a reference to the top-level ``AppDaemon`` object. This is necessary because the Logging object gets created first.""" self.AD = ad # Log Subscriptions @@ -379,21 +398,38 @@ def register_ad(self, ad): lh.setLevel(logging.INFO) self.config[log]["logger"].addHandler(lh) - # Log Objects + # Logger Objects + def get_error(self) -> Logger: + """Gets the top-level error log - def get_error(self): + Returns: + Logger: Python logger named ``Error`` + """ return self.config["error_log"]["logger"] - def get_logger(self): + def get_logger(self) -> Logger: + """Gets the top-level log + + Returns: + Logger: Python logger named ``AppDaemon`` + """ return self.config["main_log"]["logger"] - def get_access(self): + def get_access(self) -> Logger: + """ + Returns: + Logger: Python logger named ``Access`` + """ return self.config["access_log"]["logger"] - def get_diag(self): + def get_diag(self) -> Logger: + """ + Returns: + Logger: Python logger named ``Diag`` + """ return self.config["diag_log"]["logger"] - def get_filename(self, log): + def get_filename(self, log: str): return self.config[log]["filename"] def get_user_log(self, app, log): @@ -402,7 +438,19 @@ def get_user_log(self, app, log): return None return self.config[log]["logger"] - def get_child(self, name): + def get_child(self, name: str) -> Logger: + """Creates a logger with the name ``AppDaemon.``. Automatically adds a :class:`~DuplicateFilter` with the config options from ``main_log``: + + - filter_threshold + - filter_repeat_delay + - filter_timeout + + Args: + name (str): Child name for the logger. + + Returns: + Logger: Child logger + """ logger = self.get_logger().getChild(name) logger.addFilter( DuplicateFilter( @@ -458,12 +506,12 @@ def is_alias(self, log): return True return False - async def add_log_callback(self, namespace, name, cb, level, **kwargs): + async def add_log_callback(self, namespace: str, name: str, cb: Callable, level, **kwargs): """Adds a callback for log which is called internally by apps. Args: - name (str): Name of the app. namespace (str): Namespace of the log event. + name (str): Name of the app. cb: Callback function. event (str): Name of the event. **kwargs: List of values to filter on, and additional arguments to pass to the callback. @@ -598,16 +646,12 @@ async def process_log_callbacks(self, namespace, log_data): for remove in removes: await self.cancel_log_callback(remove["name"], remove["uuid"]) - async def cancel_log_callback(self, name, handles): - """Cancels an log callback. + async def cancel_log_callback(self, name: str, handles: Union[str, List[str]]): + """Cancels log callback(s). Args: name (str): Name of the app or module. - handle: Previously supplied callback handle for the callback. - - Returns: - None. - + handles (Union[str, List[str]]): Callback handle or list of them """ executed = False diff --git a/appdaemon/plugin_management.py b/appdaemon/plugin_management.py index 04548e540..497fd0cab 100644 --- a/appdaemon/plugin_management.py +++ b/appdaemon/plugin_management.py @@ -3,12 +3,16 @@ import os import sys import traceback +from logging import Logger +from typing import TYPE_CHECKING, Any, Dict, Union import async_timeout import appdaemon.utils as utils from appdaemon.app_management import UpdateMode -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class PluginBase: @@ -16,14 +20,15 @@ class PluginBase: Base class for plugins to set up _logging """ - AD: AppDaemon + AD: "AppDaemon" + logger: Logger bytes_sent: int bytes_recv: int requests_sent: int updates_recv: int last_check_ts: int - def __init__(self, ad: AppDaemon, name, args): + def __init__(self, ad: "AppDaemon", name, args): self.AD = ad self.logger = self.AD.logging.get_child(name) @@ -38,7 +43,7 @@ def __init__(self, ad: AppDaemon, name, args): def set_log_level(self, level): self.logger.setLevel(self.AD.logging.log_levels[level]) - async def perf_data(self): + async def perf_data(self) -> Dict[str, Union[int, float]]: data = { "bytes_sent": self.bytes_sent, "bytes_recv": self.bytes_recv, @@ -63,20 +68,27 @@ def update_perf(self, **kwargs): class Plugins: - """Subsystem container for managing plugins + """Subsystem container for managing plugins""" - Attributes: - AD: Reference to the AppDaemon container object - plugin_meta: Dictionary storing the metadata for the loaded plugins - plugin_objs: Dictionary storing the instantiated plugin objects + AD: "AppDaemon" + """Reference to the top-level AppDaemon container object + """ + logger: Logger + """Standard python logger named ``AppDaemon._plugin_management`` + """ + error: Logger + """Standard python logger named ``Error`` """ - - AD: AppDaemon stopping: bool - plugin_meta: dict[str, dict] + plugin_meta: Dict[str, dict] + """Dictionary storing the metadata for the loaded plugins + """ + plugin_objs: Dict[str, Any] + """Dictionary storing the instantiated plugin objects + """ required_meta = ["latitude", "longitude", "elevation", "time_zone"] - def __init__(self, ad: AppDaemon, kwargs): + def __init__(self, ad: "AppDaemon", kwargs): self.AD = ad self.plugins = kwargs self.stopping = False @@ -232,7 +244,7 @@ def process_meta(self, meta, namespace): def get_plugin(self, plugin): return self.plugins[plugin] - async def get_plugin_object(self, namespace): + async def get_plugin_object(self, namespace: str): if namespace in self.plugin_objs: return self.plugin_objs[namespace]["object"] diff --git a/appdaemon/scheduler.py b/appdaemon/scheduler.py index 1ae80b075..480021b3d 100644 --- a/appdaemon/scheduler.py +++ b/appdaemon/scheduler.py @@ -7,16 +7,25 @@ import uuid from collections import OrderedDict from datetime import timedelta +from logging import Logger +from typing import TYPE_CHECKING import pytz from astral.location import Location, LocationInfo import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Scheduler: - def __init__(self, ad: AppDaemon): + AD: "AppDaemon" + logger: Logger + error: Logger + diag: Logger + + def __init__(self, ad: "AppDaemon"): self.AD = ad self.logger = ad.logging.get_child("_scheduler") diff --git a/appdaemon/sequences.py b/appdaemon/sequences.py index b023d1d4f..724367ae1 100644 --- a/appdaemon/sequences.py +++ b/appdaemon/sequences.py @@ -1,12 +1,16 @@ -import uuid import asyncio -import traceback import copy +import traceback +import uuid +from logging import Logger +from typing import TYPE_CHECKING -from appdaemon.appdaemon import AppDaemon from appdaemon.entity import Entity from appdaemon.exceptions import TimeOutException +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon + class Sequences: """Subsystem container for managing sequences @@ -15,9 +19,10 @@ class Sequences: AD: Reference to the AppDaemon container object """ - AD: AppDaemon + AD: "AppDaemon" + logger: Logger - def __init__(self, ad: AppDaemon): + def __init__(self, ad: "AppDaemon"): self.AD = ad self.logger = ad.logging.get_child("_sequences") diff --git a/appdaemon/services.py b/appdaemon/services.py index 0ad8ea395..b8049a819 100644 --- a/appdaemon/services.py +++ b/appdaemon/services.py @@ -1,12 +1,15 @@ +import asyncio import threading import traceback -import asyncio from copy import deepcopy -from typing import Any, Optional, Callable, Awaitable +from logging import Logger +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Optional, Set -from appdaemon.appdaemon import AppDaemon -from appdaemon.exceptions import NamespaceException, DomainException, ServiceException import appdaemon.utils as utils +from appdaemon.exceptions import DomainException, NamespaceException, ServiceException + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Services: @@ -16,9 +19,13 @@ class Services: AD: Reference to the AppDaemon container object """ - AD: AppDaemon + AD: "AppDaemon" + logger: Logger + services: Dict[str, Dict[str, Any]] + services_lock: threading.RLock + app_registered_services: Dict[str, Set] - def __init__(self, ad: AppDaemon): + def __init__(self, ad: "AppDaemon"): self.AD = ad self.services = {} self.services_lock = threading.RLock() @@ -26,7 +33,7 @@ def __init__(self, ad: AppDaemon): self.logger = ad.logging.get_child("_services") def register_service( - self, namespace: str, domain: str, service: str, callback: Callable, **kwargs: Optional[Any] + self, namespace: str, domain: str, service: str, callback: Callable, **kwargs: Optional[Dict[str, Any]] ) -> None: self.logger.debug( "register_service called: %s.%s.%s -> %s", diff --git a/appdaemon/state.py b/appdaemon/state.py index 94996fa4b..7e20c2f92 100644 --- a/appdaemon/state.py +++ b/appdaemon/state.py @@ -1,11 +1,15 @@ -import uuid -import traceback +import datetime import os +import traceback +import uuid from copy import copy, deepcopy -import datetime +from logging import Logger +from typing import TYPE_CHECKING import appdaemon.utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class State: @@ -15,7 +19,10 @@ class State: AD: Reference to the AppDaemon container object """ - def __init__(self, ad: AppDaemon): + AD: "AppDaemon" + logger: Logger + + def __init__(self, ad: "AppDaemon"): self.AD = ad self.state = {"default": {}, "admin": {}, "rules": {}} diff --git a/appdaemon/thread_async.py b/appdaemon/thread_async.py index fd4023be9..e796ba938 100644 --- a/appdaemon/thread_async.py +++ b/appdaemon/thread_async.py @@ -1,7 +1,10 @@ import asyncio import traceback +from logging import Logger +from typing import TYPE_CHECKING -from appdaemon.appdaemon import AppDaemon +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class ThreadAsync: @@ -9,14 +12,15 @@ class ThreadAsync: Module to translate from the thread world to the async world via queues """ - def __init__(self, ad: AppDaemon): + AD: "AppDaemon" + stopping: bool + logging: Logger + appq: asyncio.Queue + + def __init__(self, ad: "AppDaemon"): self.AD = ad self.stopping = False self.logger = ad.logging.get_child("_thread_async") - # - # Initial Setup - # - self.appq = asyncio.Queue(maxsize=0) def stop(self): diff --git a/appdaemon/threading.py b/appdaemon/threading.py index 793422046..745eba529 100644 --- a/appdaemon/threading.py +++ b/appdaemon/threading.py @@ -7,17 +7,44 @@ import threading import traceback from datetime import timedelta +from logging import Logger from queue import Queue from random import randint +from threading import Thread +from typing import TYPE_CHECKING, Dict, List, Optional, Union import iso8601 from appdaemon import utils as utils -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Threading: - def __init__(self, ad: AppDaemon, kwargs): + """Subsystem container for managing :class:`~threading.Thread` objects""" + + AD: "AppDaemon" + """Reference to the AppDaemon container object + """ + logger: Logger + """Standard python logger named ``AppDaemon._threading`` + """ + diag: Logger + """Standard python logger named ``Diag`` + """ + thread_count: int + threads: Dict[str, Dict[str, Union[Thread, Queue]]] + """Dictionary with keys of the thread ID (string beginning with `thread-`) and values of another dictionary with `thread` and `queue` keys that have values of :class:`~threading.Thread` and :class:`~queue.Queue` objects respectively.""" + auto_pin: bool + pin_threads: int + total_threads: int + pin_apps: Optional[bool] + next_thread: Optional[int] + last_stats_time: datetime.datetime + callback_list: List[Dict] + + def __init__(self, ad: "AppDaemon", kwargs): self.AD = ad self.kwargs = kwargs @@ -49,11 +76,17 @@ def __init__(self, ad: AppDaemon, kwargs): self.callback_list = [] async def get_q_update(self): + """Updates queue sizes""" for thread in self.threads: qsize = self.get_q(thread).qsize() await self.set_state("_threading", "admin", "thread.{}".format(thread), q=qsize) async def get_callback_update(self): + """Updates the sensors with information about how many callbacks have been fired. Called by the :class:`~appdaemon.admin_loop.AdminLoop` + + - ``sensor.callbacks_average_fired`` + - ``sensor.callbacks_average_executed`` + """ now = datetime.datetime.now() self.callback_list.append( {"fired": self.current_callbacks_fired, "executed": self.current_callbacks_executed, "ts": now} @@ -170,7 +203,7 @@ async def create_initial_threads(self): }, ) - def get_q(self, thread_id): + def get_q(self, thread_id: str) -> Queue: return self.threads[thread_id]["queue"] @staticmethod diff --git a/appdaemon/utility_loop.py b/appdaemon/utility_loop.py index 24f2ded99..b804c8206 100644 --- a/appdaemon/utility_loop.py +++ b/appdaemon/utility_loop.py @@ -3,10 +3,14 @@ import asyncio import datetime import traceback +from logging import Logger +from typing import TYPE_CHECKING import appdaemon.utils as utils from appdaemon.app_management import UpdateMode -from appdaemon.appdaemon import AppDaemon + +if TYPE_CHECKING: + from appdaemon.appdaemon import AppDaemon class Utility: @@ -15,10 +19,14 @@ class Utility: Checks for file changes, overdue threads, thread starvation, and schedules regular state refreshes. """ - AD: AppDaemon + AD: "AppDaemon" + """Reference to the AppDaemon container object + """ + stopping: bool + logger: Logger - def __init__(self, ad: AppDaemon): + def __init__(self, ad: "AppDaemon"): """Constructor. Args: @@ -29,6 +37,7 @@ def __init__(self, ad: AppDaemon): self.stopping = False self.logger = ad.logging.get_child("_utility") self.booted = None + # self.AD.loop.create_task(self.loop()) def stop(self): """Called by the AppDaemon object to terminate the loop cleanly diff --git a/appdaemon/utils.py b/appdaemon/utils.py index d6a05a5a0..929d05ef9 100644 --- a/appdaemon/utils.py +++ b/appdaemon/utils.py @@ -20,7 +20,7 @@ from datetime import timedelta from functools import wraps from types import ModuleType -from typing import Callable +from typing import Any, Callable, Dict import dateutil.parser import tomli @@ -293,17 +293,30 @@ def day_of_week(day): nums = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] days = {day: idx for idx, day in enumerate(nums)} - if type(day) == str: + if isinstance(day, str): return days[day] - if type(day) == int: + if isinstance(day, int): return nums[day] raise ValueError("Incorrect type for 'day' in day_of_week()'") -async def run_in_executor(self, fn, *args, **kwargs): - completed, pending = await asyncio.wait( - [self.AD.loop.run_in_executor(self.AD.executor, functools.partial(fn, *args, **kwargs))] - ) +async def run_in_executor(self, fn, *args, **kwargs) -> Any: + """Runs the function with the given arguments in the instance of :class:`~concurrent.futures.ThreadPoolExecutor` in the top-level :class:`~appdaemon.appdaemon.AppDaemon` object. + + Args: + self: Needs to have an ``AD`` attribute with the :class:`~appdaemon.appdaemon.AppDaemon` object + fn (function): Function to run in the executor + *args: Any positional arguments to use with the function + **kwargs: Any keyword arguments to use with the function + + Returns: + Whatever the function returns + """ + loop: asyncio.BaseEventLoop = self.AD.loop + executor: concurrent.futures.ThreadPoolExecutor = self.AD.executor + preloaded_function = functools.partial(fn, *args, **kwargs) + + completed, pending = await asyncio.wait([loop.run_in_executor(executor, preloaded_function)]) future = list(completed)[0] response = future.result() return response @@ -370,7 +383,7 @@ def deepcopy(data): return result -def find_path(name): +def find_path(name: str): for path in [ os.path.join(os.path.expanduser("~"), ".homeassistant"), os.path.join(os.path.sep, "etc", "appdaemon"), @@ -584,7 +597,8 @@ def write_toml_config(path, **kwargs): tomli_w.dump(kwargs, stream) -def read_config_file(path): +def read_config_file(path) -> Dict[str, Dict]: + """Reads a single YAML or TOML file.""" extension = os.path.splitext(path)[1] if extension == ".yaml": return read_yaml_config(path) @@ -699,7 +713,7 @@ def _include_yaml(loader, node): return yaml.load(f, Loader=yaml.SafeLoader) -def read_yaml_config(config_file_yaml): +def read_yaml_config(config_file_yaml) -> Dict[str, Dict]: # # First locate secrets file # diff --git a/docs/INTERNALS.rst b/docs/INTERNALS.rst index a71c7349f..f844b5d3a 100644 --- a/docs/INTERNALS.rst +++ b/docs/INTERNALS.rst @@ -40,16 +40,30 @@ events .. automodule:: appdaemon.events :members: +futures +======= +.. automodule:: appdaemon.futures + :members: + +http +====== +.. automodule:: appdaemon.http + :members: + logging ======= .. automodule:: appdaemon.logging :members: +main +==== + .. automodule:: appdaemon.__main__ :members: -main -==== +plugins +======= + .. automodule:: appdaemon.plugin_management :members: diff --git a/docs/conf.py b/docs/conf.py index 664369b65..eb0933e8f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,8 +6,8 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the