44import json
55import logging
66import os
7+ import sys
78
9+ import pkg_resources
810import voluptuous as vol
911
1012from homeassistant .config import async_hass_config_yaml
1921import homeassistant .helpers .config_validation as cv
2022from homeassistant .helpers .restore_state import RestoreStateData
2123from homeassistant .loader import bind_hass
24+ from homeassistant .requirements import async_process_requirements
2225
2326from .const import (
2427 CONF_ALLOW_ALL_IMPORTS ,
3841from .state import State
3942from .trigger import TrigTime
4043
44+ if sys .version_info [:2 ] >= (3 , 8 ):
45+ from importlib .metadata import ( # pylint: disable=no-name-in-module,import-error
46+ PackageNotFoundError ,
47+ version ,
48+ )
49+ else :
50+ from importlib_metadata import ( # pylint: disable=import-error
51+ PackageNotFoundError ,
52+ version ,
53+ )
54+
4155_LOGGER = logging .getLogger (LOGGER_PATH )
4256
4357PYSCRIPT_SCHEMA = vol .Schema (
@@ -133,6 +147,7 @@ async def async_setup_entry(hass, config_entry):
133147
134148 State .set_pyscript_config (config_entry .data )
135149
150+ await install_requirements (hass )
136151 await load_scripts (hass , config_entry .data )
137152
138153 async def reload_scripts_handler (call ):
@@ -150,6 +165,7 @@ async def reload_scripts_handler(call):
150165
151166 await unload_scripts (global_ctx_only = global_ctx_only )
152167
168+ await install_requirements (hass )
153169 await load_scripts (hass , config_entry .data , global_ctx_only = global_ctx_only )
154170
155171 start_global_contexts (global_ctx_only = global_ctx_only )
@@ -250,6 +266,55 @@ async def unload_scripts(global_ctx_only=None, unload_all=False):
250266 await GlobalContextMgr .delete (global_ctx_name )
251267
252268
269+ @bind_hass
270+ async def install_requirements (hass ):
271+ """Install missing requirements from requirements.txt."""
272+ requirements_path = os .path .join (hass .config .path (FOLDER ), "requirements.txt" )
273+
274+ if os .path .exists (requirements_path ):
275+ with open (requirements_path , "r" ) as requirements_file :
276+ requirements_to_install = []
277+ for pkg in requirements_file .readlines ():
278+ # Remove inline comments which are accepted by pip but not by Home
279+ # Assistant's installation method.
280+ # https://rosettacode.org/wiki/Strip_comments_from_a_string#Python
281+ i = pkg .find ("#" )
282+ if i >= 0 :
283+ pkg = pkg [:i ].strip ()
284+
285+ try :
286+ # Attempt to get version of package. Do nothing if it's found since
287+ # we want to use the version that's already installed to be safe
288+ requirement = pkg_resources .Requirement .parse (pkg )
289+ requirement_installed_version = version (requirement .project_name )
290+
291+ if requirement_installed_version in requirement :
292+ _LOGGER .debug ("`%s` already found" , requirement .project_name )
293+ else :
294+ _LOGGER .debug (
295+ (
296+ "`%s` already found but found version `%s` does not"
297+ " match requirement. Keeping found version."
298+ ),
299+ requirement .project_name ,
300+ requirement_installed_version ,
301+ )
302+ except PackageNotFoundError :
303+ # Since package wasn't found, add it to installation list
304+ _LOGGER .debug ("%s not found, adding it to package installation list" , pkg )
305+ requirements_to_install .append (pkg )
306+ except ValueError :
307+ # Not valid requirements line so it can be skipped
308+ _LOGGER .debug ("Ignoring `%s` because it is not a valid package" , pkg )
309+ if requirements_to_install :
310+ _LOGGER .info ("Installing the following packages: %s" , "," .join (requirements_to_install ))
311+ await async_process_requirements (hass , DOMAIN , requirements_to_install )
312+ else :
313+ _LOGGER .info ("All requirements are already available." )
314+ else :
315+ _LOGGER .info ("No requirements.txt found so nothing to install." )
316+
317+
253318@bind_hass
254319async def load_scripts (hass , data , global_ctx_only = None ):
255320 """Load all python scripts in FOLDER."""
0 commit comments