4
4
import json
5
5
import logging
6
6
import os
7
+ import sys
7
8
9
+ import pkg_resources
8
10
import voluptuous as vol
9
11
10
12
from homeassistant .config import async_hass_config_yaml
19
21
import homeassistant .helpers .config_validation as cv
20
22
from homeassistant .helpers .restore_state import RestoreStateData
21
23
from homeassistant .loader import bind_hass
24
+ from homeassistant .requirements import async_process_requirements
22
25
23
26
from .const import (
24
27
CONF_ALLOW_ALL_IMPORTS ,
27
30
DOMAIN ,
28
31
FOLDER ,
29
32
LOGGER_PATH ,
33
+ REQUIREMENTS_FILE ,
34
+ REQUIREMENTS_PATHS ,
30
35
SERVICE_JUPYTER_KERNEL_START ,
31
36
UNSUB_LISTENERS ,
32
37
)
38
43
from .state import State
39
44
from .trigger import TrigTime
40
45
46
+ if sys .version_info [:2 ] >= (3 , 8 ):
47
+ from importlib .metadata import ( # pylint: disable=no-name-in-module,import-error
48
+ PackageNotFoundError ,
49
+ version as installed_version ,
50
+ )
51
+ else :
52
+ from importlib_metadata import ( # pylint: disable=import-error
53
+ PackageNotFoundError ,
54
+ version as installed_version ,
55
+ )
56
+
41
57
_LOGGER = logging .getLogger (LOGGER_PATH )
42
58
43
59
PYSCRIPT_SCHEMA = vol .Schema (
@@ -133,6 +149,7 @@ async def async_setup_entry(hass, config_entry):
133
149
134
150
State .set_pyscript_config (config_entry .data )
135
151
152
+ await install_requirements (hass )
136
153
await load_scripts (hass , config_entry .data )
137
154
138
155
async def reload_scripts_handler (call ):
@@ -152,6 +169,7 @@ async def reload_scripts_handler(call):
152
169
153
170
await unload_scripts (global_ctx_only = global_ctx_only )
154
171
172
+ await install_requirements (hass )
155
173
await load_scripts (hass , config_entry .data , global_ctx_only = global_ctx_only )
156
174
157
175
start_global_contexts (global_ctx_only = global_ctx_only )
@@ -253,6 +271,73 @@ async def unload_scripts(global_ctx_only=None, unload_all=False):
253
271
await GlobalContextMgr .delete (global_ctx_name )
254
272
255
273
274
+ @bind_hass
275
+ def load_all_requirement_lines (hass , requirements_paths , requirements_file ):
276
+ """Load all lines from requirements_file located in requirements_paths."""
277
+ all_requirements = {}
278
+ for root in requirements_paths :
279
+ for requirements_path in glob .glob (os .path .join (hass .config .path (FOLDER ), root , requirements_file )):
280
+ with open (requirements_path , "r" ) as requirements_fp :
281
+ all_requirements [requirements_path ] = requirements_fp .readlines ()
282
+
283
+ return all_requirements
284
+
285
+
286
+ @bind_hass
287
+ async def install_requirements (hass ):
288
+ """Install missing requirements from requirements.txt."""
289
+ all_requirements = await hass .async_add_executor_job (
290
+ load_all_requirement_lines , hass , REQUIREMENTS_PATHS , REQUIREMENTS_FILE
291
+ )
292
+ requirements_to_install = []
293
+ for requirements_path , pkg_lines in all_requirements .items ():
294
+ for pkg in pkg_lines :
295
+ # Remove inline comments which are accepted by pip but not by Home
296
+ # Assistant's installation method.
297
+ # https://rosettacode.org/wiki/Strip_comments_from_a_string#Python
298
+ i = pkg .find ("#" )
299
+ if i >= 0 :
300
+ pkg = pkg [:i ]
301
+ pkg = pkg .strip ()
302
+
303
+ if not pkg :
304
+ continue
305
+
306
+ try :
307
+ # Attempt to get version of package. Do nothing if it's found since
308
+ # we want to use the version that's already installed to be safe
309
+ requirement = pkg_resources .Requirement .parse (pkg )
310
+ requirement_installed_version = installed_version (requirement .project_name )
311
+
312
+ if requirement_installed_version in requirement :
313
+ _LOGGER .debug ("`%s` already found" , requirement .project_name )
314
+ else :
315
+ _LOGGER .warning (
316
+ (
317
+ "`%s` already found but found version `%s` does not"
318
+ " match requirement. Keeping found version."
319
+ ),
320
+ requirement .project_name ,
321
+ requirement_installed_version ,
322
+ )
323
+ except PackageNotFoundError :
324
+ # Since package wasn't found, add it to installation list
325
+ _LOGGER .debug ("%s not found, adding it to package installation list" , pkg )
326
+ requirements_to_install .append (pkg )
327
+ except ValueError :
328
+ # Not valid requirements line so it can be skipped
329
+ _LOGGER .debug ("Ignoring `%s` because it is not a valid package" , pkg )
330
+ if requirements_to_install :
331
+ _LOGGER .info (
332
+ "Installing the following packages from %s: %s" ,
333
+ requirements_path ,
334
+ ", " .join (requirements_to_install ),
335
+ )
336
+ await async_process_requirements (hass , DOMAIN , requirements_to_install )
337
+ else :
338
+ _LOGGER .debug ("All packages in %s are already available" , requirements_path )
339
+
340
+
256
341
@bind_hass
257
342
async def load_scripts (hass , data , global_ctx_only = None ):
258
343
"""Load all python scripts in FOLDER."""
0 commit comments