Compare commits

..

25 Commits

Author SHA1 Message Date
John Lancaster
f6e7069449 added to launch.json example 2024-08-09 17:56:34 -05:00
John Lancaster
24dd2bfd3a more complexity in the test cases 2024-08-09 17:54:20 -05:00
John Lancaster
03ebbcc1f0 added variant 2024-07-08 17:55:32 -05:00
John Lancaster
9210cbf8a1 added parameter 2024-07-08 17:55:01 -05:00
John Lancaster
87ed3c81ae app definition organization 2024-07-07 23:39:02 -05:00
John Lancaster
4abd6ce5d5 formatting 2024-07-07 22:27:13 -05:00
John Lancaster
870424baa0 some renames and other changes 2024-07-07 22:07:02 -05:00
John Lancaster
3481ff7976 more complexity in the test conf 2024-07-07 17:17:11 -05:00
John Lancaster
6904ac30d5 added sequence 2024-07-07 17:16:19 -05:00
John Lancaster
0a177eebe8 added some globals 2024-06-22 11:15:31 -05:00
John Lancaster
1042234df7 updated README 2024-06-06 23:37:29 -05:00
John Lancaster
423465a111 added dependency 2024-06-06 23:21:19 -05:00
John Lancaster
caea4ac448 commented out new log line 2024-06-06 21:33:37 -05:00
John Lancaster
207c0d95c6 renamed to notification 2024-06-06 21:29:20 -05:00
John Lancaster
be232b4338 renames 2024-06-06 21:16:12 -05:00
John Lancaster
1135082055 more complexity to test case 2024-06-06 21:10:02 -05:00
John Lancaster
6a2877ca71 reorg and other stuff 2024-06-06 20:58:00 -05:00
John Lancaster
69134399f4 added more test apps 2024-05-13 22:12:33 -05:00
John Lancaster
fafa5cc8fc tweaks 2024-05-04 21:54:37 -05:00
John Lancaster
430cc28578 added requirements with rich 2024-05-03 08:25:43 -05:00
John Lancaster
74a6504c59 commented out print statements 2024-05-03 08:25:20 -05:00
John Lancaster
e9d5d3ce4e formatting tweaks 2024-03-18 18:53:20 -05:00
John Lancaster
c0d7871676 foo, bar, baz rename 2024-03-18 18:03:54 -05:00
John Lancaster
9c12df1256 changed port 2024-03-18 17:42:42 -05:00
John Lancaster
af433e7eb9 added module import printing 2024-03-18 17:42:29 -05:00
50 changed files with 1089 additions and 47 deletions

5
.gitignore vendored
View File

@@ -6,4 +6,7 @@ __pycache__
dashboards/ dashboards/
compiled/ compiled/
namespaces/ namespaces/
www/ www/
build/
*.egg-info

View File

@@ -6,6 +6,41 @@
Needs a long-lived token for HA in `secrets.yaml` Needs a long-lived token for HA in `secrets.yaml`
### Debugger
Use with VSCode's debugger using a `launch.json` file.
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "AppDaemon Dev",
"type": "debugpy",
"request": "launch",
"module": "appdaemon",
"justMyCode": true,
"args": "-c ../conf" // Expects the conf directory to be next to the appdaemon one
},
{
"name": "Pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"justMyCode": true,
"args": [
"${workspaceFolder}/tests",
"--maxfail=1", // Stop after the first failure
"-s" // Allow for interactive debugging (pdb)
],
}
]
}
```
### Test Script ### Test Script
```bash ```bash
@@ -20,22 +55,22 @@ cd $REPO_DIR
rm -rf ./dist rm -rf ./dist
# isort ./appdaemon ./docs/conf.py
black ./appdaemon ./docs/conf.py
pre-commit run --all-files
python -m pytest
python -m build python -m build
docker build -t appdaemon:jsl-dev . docker build -t appdaemon:local-dev .
cd ../conf cd ../conf
docker compose run -it --rm appdaemon docker compose run -it --rm appdaemon
``` ```
## TODO
### Reloading
- Seems to work from non-package module
- Does not work from inside a package module
## Reference ## Reference
- [`importlib.reload(module)`](https://docs.python.org/3/library/importlib.html#importlib.reload) - [`importlib.reload(module)`](https://docs.python.org/3/library/importlib.html#importlib.reload)

View File

@@ -1,18 +1,38 @@
appdaemon: appdaemon:
import_method: expert # ffake_ketys: asdfasdf
# starttime: '2024-07-14 12:00:00' # YYYY-MM-DD HH:MM:SS
uvloop: True
use_dictionary_unpacking: True
# import_method: expert
# import_paths: # import_paths:
# - /conf/apps/my_repo # - /conf/apps/my_repo
latitude: 0 latitude: 0
longitude: 0 longitude: 0
elevation: 30 elevation: 30
time_zone: America/Chicago time_zone: America/Chicago
# check_app_updates_profile: True
module_debug:
_app_management: DEBUG
_callbacks: DEBUG
# _events: DEBUG
# _threading: DEBUG
# mqtt: DEBUG
plugins: plugins:
HASS: hass:
type: hass type: hass
ha_url: http://192.168.1.82:8123 ha_url: http://192.168.1.82:8123
token: !secret long_lived_token token: !secret long_lived_token
http: # mqtt:
url: http://0.0.0.0:5050 # type: mqtt
# namespace: mqtt
# client_host: zigbee.localdomain
# client_user: homeassistant
# client_id: test_dev_client
# client_password: !secret mqtt_password
# client_topics:
# - zigbee2mqtt/#
# http:
# url: http://0.0.0.0:5051
admin: admin:
api: api:
hadashboard: hadashboard:

29
apps/app1/button.py Normal file
View File

@@ -0,0 +1,29 @@
import json
from appdaemon.adapi import ADAPI
class Button(ADAPI):
def initialize(self):
name = self.args['button']
self.handle = self.listen_event(
self.handle_button,
'MQTT_MESSAGE',
topic=f'zigbee2mqtt/{name}',
payload=self.payload_filter,
)
self.log(f"Started MQTT app in namespace '{self._namespace}'")
# raise ValueError
@staticmethod
def payload_filter(payload: str):
try:
return json.loads(payload)['action'] != ''
except Exception:
return False
def handle_button(self, event_name, data, **kwargs):
data['payload'] = json.loads(data['payload'])
# if data['payload']['action'] != '':
json_str = json.dumps(data, indent=4)
self.logger.info(f'{event_name} callback with\n{json_str}\n{kwargs}')

12
apps/app1/database.py Normal file
View File

@@ -0,0 +1,12 @@
from appdaemon.adapi import ADAPI
class Database(ADAPI):
def initialize(self):
self.comp = DatabaseComponent(self)
self.log('Database with component')
class DatabaseComponent:
def __init__(self, ad: ADAPI) -> None:
self.ad = ad

12
apps/app1/notification.py Normal file
View File

@@ -0,0 +1,12 @@
from appdaemon.adapi import ADAPI
class Notification:
def __init__(self, ad: ADAPI, name: str) -> None:
self.ad = ad
self.log = self.ad.logger.info
self.log(f'Notification initialized for {name}')
# self.ad.logger.info(' NOTIFICATION '.center(50, '-'))
def send(self, message: str = 'message not specified') -> None:
self.ad.log(f'NOTIFICATION: {message}', level='DEBUG')

View File

13
apps/app1/subdir/foo.py Normal file
View File

@@ -0,0 +1,13 @@
from pathlib import Path
from appdaemon.adapi import ADAPI
from ..notification import Notification
class Foo(ADAPI):
def initialize(self):
# self.log('FOO')
self.log(f'Initialized app from {Path(__file__).relative_to(self.AD.app_dir.parent)}')
self.notification = Notification(self.AD, self.name)
# self.log(' FOO '.center(50, '*'))

8
apps/app2/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
import appdaemon
# import app2.database
from . import database
# from .database import OtherDatabaseApp
# from .database import OtherDatabaseApp as NormalApp

8
apps/app2/database.py Normal file
View File

@@ -0,0 +1,8 @@
from app1.subdir.foo import Foo
class OtherDatabaseApp(Foo):
def initialize(self):
super().initialize()
self.log('This app inherits from a custom class')
# self.log(' CHANGE '.center(50, '-'))

View File

@@ -1,11 +1,3 @@
hello_world: hello_world:
module: hello module: hello
class: HelloWorld class: HelloWorld
hello_sub:
module: my_pkg.my_sub_pkg.hello
class: HelloWorldSub
motion:
module: my_pkg
class: Motion

View File

@@ -0,0 +1,13 @@
import arrow
from appdaemon.adapi import ADAPI
class AutomationLib:
"""This is the documentation for AutomationLib"""
def __init__(self, ad: ADAPI) -> None:
self.ad = ad
self.ad.log(f"AutomationLib initialised for app '{self.ad.name}'", level='DEBUG')
def dow(self):
return arrow.now().isoweekday()

View File

@@ -0,0 +1,10 @@
automationlib:
module: automationlib
global: true
my_test_app:
module: test
class: Test
log_level: DEBUG
dependencies:
- automationlib

View File

@@ -0,0 +1 @@
arrow

View File

@@ -0,0 +1,14 @@
from appdaemon.plugins.hass.hassapi import Hass
from automationlib import AutomationLib
class Test(Hass):
"""This is the documentation for Test"""
def initialize(self):
"""."""
self.lib = AutomationLib(self)
self.log('-' * 72)
self.log(f'Day of the week: {self.lib.dow()}')
self.log('-' * 72)

6
apps/button.yaml Normal file
View File

@@ -0,0 +1,6 @@
button_test:
module: app1.button
class: Button
button: Living Room Button
dependencies:
- hello_world

25
apps/databases.yaml Normal file
View File

@@ -0,0 +1,25 @@
App1:
module: app1.database
class: Database
other_kwargs: value
dependencies: Sound
App1Foo:
module: app1.subdir.foo
class: Foo
# dependencies:
# - companion_app
App2:
module: app2.database
class: OtherDatabaseApp
dependencies:
- App1
- App1Foo
- globals
App3:
module: app2.database
class: OtherDatabaseApp
dependencies:
- App2

0
apps/family/__init__.py Normal file
View File

10
apps/family/child.py Normal file
View File

@@ -0,0 +1,10 @@
from appdaemon.adapi import ADAPI
from .utils import UTILS_VALS
raise ImportError('Fake import error')
class Child(ADAPI):
def initialize(self):
self.log(f'Initialized child with: {UTILS_VALS}')

7
apps/family/family.yaml Normal file
View File

@@ -0,0 +1,7 @@
parent:
module: family.parent
class: Parent
child:
module: family.child
class: Child

8
apps/family/parent.py Normal file
View File

@@ -0,0 +1,8 @@
from appdaemon.adapi import ADAPI
from .utils import UTILS_VALS
class Parent(ADAPI):
def initialize(self):
self.log(f'Initialized parent with: {UTILS_VALS}')

View File

View File

@@ -0,0 +1 @@
OTHER_CITY = 'NYC again'

3
apps/family/utils.py Normal file
View File

@@ -0,0 +1,3 @@
from .relatives.city import OTHER_CITY
UTILS_VALS = 100, 600

10
apps/global_apps.yaml Normal file
View File

@@ -0,0 +1,10 @@
globals:
module: globals
global: true
# companion_app:
# module: app1.notification
# global: true
global_modules:
- hello

View File

@@ -0,0 +1 @@
ALEXA = True

View File

@@ -0,0 +1 @@
FRIGATE = True

View File

@@ -0,0 +1 @@
GLOBAL_CONSTANT = True

View File

@@ -1,7 +1,8 @@
import appdaemon.adbase as ad from pathlib import Path
class HelloWorld(ad.ADBase): from appdaemon.adapi import ADAPI
class HelloWorld(ADAPI):
def initialize(self): def initialize(self):
self.adapi = self.get_ad_api() self.log(f'Initialized app from {Path(__file__).relative_to(Path(self.AD.app_dir).parent)}')
self.log = self.adapi.log
self.log(f'Initialized app from {__file__}')

View File

@@ -1,2 +0,0 @@
from .motion import Motion
from .my_sub_pkg.hello import HelloWorldSub

View File

@@ -1,8 +0,0 @@
import appdaemon.adbase as ad
class Motion(ad.ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
self.log = self.adapi.log
self.log(f'Initialized app from {__file__}')
# self.log(f'New log line asdfasdf')

View File

@@ -1,8 +0,0 @@
import appdaemon.adbase as ad
class HelloWorldSub(ad.ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
self.log = self.adapi.log
self.log(f'Initialized app from {__file__}')
# self.log(f'CHANGED')

View File

@@ -0,0 +1,4 @@
[project]
name = "my_pkg"
description="Config for AppDaemon testing"
version = "0.1.0"

View File

@@ -0,0 +1,667 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import ast\n",
"import importlib\n",
"import sys\n",
"from pathlib import Path\n",
"from types import ModuleType\n",
"from typing import Dict, Iterable, Iterator, Mapping, Set, Tuple\n",
"\n",
"from rich import print"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from appdaemon.dependency import (\n",
" Graph,\n",
" find_all_dependents,\n",
" get_all_nodes,\n",
" get_dependency_graph,\n",
" get_file_deps,\n",
" get_full_module_name,\n",
" resolve_relative_import,\n",
" reverse_graph,\n",
" topo_sort,\n",
")\n",
"from appdaemon.models.internal.file_check import FileCheck"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Logging"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"import logging.config\n",
"\n",
"from rich.console import Console\n",
"from rich.highlighter import NullHighlighter\n",
"\n",
"console = Console()\n",
"\n",
"logging.config.dictConfig(\n",
" {\n",
" 'version': 1,\n",
" 'disable_existing_loggers': False,\n",
" 'formatters': {'basic': {'style': '{', 'format': '{message}'}},\n",
" 'handlers': {\n",
" 'rich': {\n",
" '()': 'rich.logging.RichHandler',\n",
" 'formatter': 'basic',\n",
" 'console': console,\n",
" 'highlighter': NullHighlighter(),\n",
" 'markup': True\n",
" }\n",
" },\n",
" 'root': {'level': 'DEBUG', 'handlers': ['rich']},\n",
" }\n",
")\n",
"\n",
"logger = logging.getLogger()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## App Load Example\n",
"\n",
"Example of using importlib to (re)load an app class from a module."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"change\n"
]
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">&lt;</span><span style=\"color: #ff00ff; text-decoration-color: #ff00ff; font-weight: bold\">class</span><span style=\"color: #000000; text-decoration-color: #000000\"> </span><span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg.deep_mod.Deep'</span><span style=\"font-weight: bold\">&gt;</span>\n",
"</pre>\n"
],
"text/plain": [
"\u001b[1m<\u001b[0m\u001b[1;95mclass\u001b[0m\u001b[39m \u001b[0m\u001b[32m'my_pkg.my_sub_pkg.deep_pkg.deep_mod.Deep'\u001b[0m\u001b[1m>\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# apps_path = '/home/john/conf/apps'\n",
"# module_name = 'app2'\n",
"# class_name = 'NormalApp'\n",
"\n",
"apps_path = '/home/john/conf/apps/my_repo/src'\n",
"module_name = 'my_pkg.my_sub_pkg.deep_pkg'\n",
"class_name = 'Deep'\n",
"\n",
"if apps_path not in sys.path:\n",
" sys.path.insert(0, apps_path)\n",
"\n",
"if mod := sys.modules.get(module_name):\n",
" importlib.reload(mod)\n",
"else:\n",
" mod = importlib.import_module(module_name)\n",
"\n",
"class_def = getattr(mod, class_name)\n",
"print(class_def)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['my_pkg',\n",
" 'my_pkg.foo',\n",
" 'my_pkg.my_sub_pkg',\n",
" 'my_pkg.my_sub_pkg.bar',\n",
" 'my_pkg.my_sub_pkg.deep_pkg',\n",
" 'my_pkg.my_sub_pkg.deep_pkg.deep_mod']"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"pkgs = sorted(name for name in sys.modules.keys() if name.startswith('my'))\n",
"pkgs"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## File Mapping\n",
"\n",
"Mapping each file to the package string it will be imported from, so it can be retrieved from `sys.modules`\n",
"\n",
"Flow\n",
"\n",
"- List of changed files\n",
"- Package associated with each file\n",
"- Packages that each file depends on"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"app_dir = Path('/home/john/conf/apps')\n",
"# files_to_modules = paths_to_modules(app_dir)\n",
"# print(files_to_modules)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Dependencies\n",
"\n",
"Find which modules each module depends on.\n",
"\n",
"```python\n",
"deps = {\n",
" 'A': {'B', 'C'},\n",
" 'B': {'C'},\n",
" 'C': {'D'},\n",
" 'D': set()\n",
"}\n",
"```\n",
"\n",
"`A` depends on `B` and `C`.\n",
"\n",
"If `D` needs to get reloaded, what else does too?"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"color: #00ff00; text-decoration-color: #00ff00; font-style: italic\">True</span>\n",
"</pre>\n"
],
"text/plain": [
"\u001b[3;92mTrue\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"{'C': {'A', 'B'}, 'D': {'C'}, 'B': {'A'}, 'A': set()}"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"deps = {'A': {'B', 'C'}, 'B': {'C'}, 'C': {'D'}, 'D': set()}\n",
"\n",
"reversable = deps == reverse_graph(reverse_graph(deps))\n",
"print(reversable)\n",
"\n",
"reverse_graph(deps)"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">{</span>\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'hello'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'pathlib'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'logging.config'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adapi'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'logging'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'globals'</span>: <span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">set</span><span style=\"font-weight: bold\">()</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.foo'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.bar'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.foo'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'pathlib'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adbase'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.bar'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'pathlib'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adbase'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.bar'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.baz'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'pathlib'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adbase'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg.deep_mod'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'pathlib'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adbase'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.foo'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg.deep_mod'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.bar'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'app1.notification'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adapi'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'app1'</span>: <span style=\"color: #800080; text-decoration-color: #800080; font-weight: bold\">set</span><span style=\"font-weight: bold\">()</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'app1.database'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adapi'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'foo'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adapi'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'test'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'app1.notification'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adbase'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'app2'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'app2.database'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon'</span><span style=\"font-weight: bold\">}</span>,\n",
" <span style=\"color: #008000; text-decoration-color: #008000\">'app2.database'</span>: <span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'appdaemon.adapi'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'app1.subdir.foo'</span><span style=\"font-weight: bold\">}</span>\n",
"<span style=\"font-weight: bold\">}</span>\n",
"</pre>\n"
],
"text/plain": [
"\u001b[1m{\u001b[0m\n",
" \u001b[32m'hello'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'pathlib'\u001b[0m, \u001b[32m'logging.config'\u001b[0m, \u001b[32m'appdaemon.adapi'\u001b[0m, \u001b[32m'logging'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'globals'\u001b[0m: \u001b[1;35mset\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[32m'my_pkg'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'my_pkg.foo'\u001b[0m, \u001b[32m'my_pkg.my_sub_pkg.bar'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.foo'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'pathlib'\u001b[0m, \u001b[32m'appdaemon.adbase'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.my_sub_pkg.bar'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'pathlib'\u001b[0m, \u001b[32m'appdaemon.adbase'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.my_sub_pkg'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'my_pkg.my_sub_pkg.bar'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.my_sub_pkg.baz'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'pathlib'\u001b[0m, \u001b[32m'appdaemon.adbase'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.my_sub_pkg.deep_pkg.deep_mod'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'pathlib'\u001b[0m, \u001b[32m'appdaemon.adbase'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'my_pkg.my_sub_pkg.deep_pkg'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'my_pkg.foo'\u001b[0m, \u001b[32m'my_pkg.my_sub_pkg.deep_pkg.deep_mod'\u001b[0m, \u001b[32m'my_pkg.my_sub_pkg.bar'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'app1.notification'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'appdaemon.adapi'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'app1'\u001b[0m: \u001b[1;35mset\u001b[0m\u001b[1m(\u001b[0m\u001b[1m)\u001b[0m,\n",
" \u001b[32m'app1.database'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'appdaemon.adapi'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'foo'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'appdaemon.adapi'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'test'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'app1.notification'\u001b[0m, \u001b[32m'appdaemon.adbase'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'app2'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'app2.database'\u001b[0m, \u001b[32m'appdaemon'\u001b[0m\u001b[1m}\u001b[0m,\n",
" \u001b[32m'app2.database'\u001b[0m: \u001b[1m{\u001b[0m\u001b[32m'appdaemon.adapi'\u001b[0m, \u001b[32m'app1.subdir.foo'\u001b[0m\u001b[1m}\u001b[0m\n",
"\u001b[1m}\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"module_dependencies = get_dependency_graph(app_dir.rglob('*.py'))\n",
"print(module_dependencies)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\"><span style=\"font-weight: bold\">{</span><span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg.deep_mod'</span>, <span style=\"color: #008000; text-decoration-color: #008000\">'my_pkg.my_sub_pkg.deep_pkg'</span><span style=\"font-weight: bold\">}</span>\n",
"</pre>\n"
],
"text/plain": [
"\u001b[1m{\u001b[0m\u001b[32m'my_pkg.my_sub_pkg.deep_pkg.deep_mod'\u001b[0m, \u001b[32m'my_pkg.my_sub_pkg.deep_pkg'\u001b[0m\u001b[1m}\u001b[0m\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"files = [\n",
" Path('/home/john/conf/apps/my_repo/src/my_pkg/my_sub_pkg/deep_pkg/__init__.py'),\n",
" Path('/home/john/conf/apps/my_repo/src/my_pkg/my_sub_pkg/deep_pkg/deep_mod.py'),\n",
"]\n",
"\n",
"changed_pkg = set(get_full_module_name(f) for f in files)\n",
"reversed_deps = reverse_graph(module_dependencies)\n",
"dependents = find_all_dependents(changed_pkg, reversed_deps)\n",
"to_load = changed_pkg | dependents\n",
"print(to_load)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">--------------------------------------------------\n",
"</pre>\n"
],
"text/plain": [
"--------------------------------------------------\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">from <span style=\"color: #808000; text-decoration-color: #808000\">...</span>foo import Foo\n",
"</pre>\n"
],
"text/plain": [
"from \u001b[33m...\u001b[0mfoo import Foo\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">my_pkg.foo\n",
"</pre>\n"
],
"text/plain": [
"my_pkg.foo\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">--------------------------------------------------\n",
"</pre>\n"
],
"text/plain": [
"--------------------------------------------------\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">from ..bar import Bar\n",
"</pre>\n"
],
"text/plain": [
"from ..bar import Bar\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">my_pkg.my_sub_pkg.bar\n",
"</pre>\n"
],
"text/plain": [
"my_pkg.my_sub_pkg.bar\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">--------------------------------------------------\n",
"</pre>\n"
],
"text/plain": [
"--------------------------------------------------\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">from . import deep_mod\n",
"</pre>\n"
],
"text/plain": [
"from . import deep_mod\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">my_pkg.my_sub_pkg.deep_pkg\n",
"</pre>\n"
],
"text/plain": [
"my_pkg.my_sub_pkg.deep_pkg\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">--------------------------------------------------\n",
"</pre>\n"
],
"text/plain": [
"--------------------------------------------------\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">from .. import bar\n",
"</pre>\n"
],
"text/plain": [
"from .. import bar\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">my_pkg.my_sub_pkg\n",
"</pre>\n"
],
"text/plain": [
"my_pkg.my_sub_pkg\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">--------------------------------------------------\n",
"</pre>\n"
],
"text/plain": [
"--------------------------------------------------\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">from .deep_mod import Deep\n",
"</pre>\n"
],
"text/plain": [
"from .deep_mod import Deep\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"<pre style=\"white-space:pre;overflow-x:auto;line-height:normal;font-family:Menlo,'DejaVu Sans Mono',consolas,'Courier New',monospace\">my_pkg.my_sub_pkg.deep_pkg.deep_mod\n",
"</pre>\n"
],
"text/plain": [
"my_pkg.my_sub_pkg.deep_pkg.deep_mod\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"def test_rel_imports(file: Path):\n",
" with file.open('r') as f:\n",
" file_content = f.read()\n",
" \n",
" lines = file_content.splitlines()\n",
"\n",
" tree = ast.parse(file_content, filename=file)\n",
" for node in ast.walk(tree):\n",
" if isinstance(node, ast.ImportFrom):\n",
" abs_module = resolve_relative_import(node, file)\n",
" print('-' * 50)\n",
" print(lines[node.lineno-1])\n",
" print(abs_module)\n",
"\n",
"test_rel_imports(files[0])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Partial Dependency Graph\n",
"\n",
"This is an example of generating the dependency graph using an iterable of file Paths."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"p = Path('/home/john/conf/apps/my_repo/src/my_pkg/my_sub_pkg')\n",
"files = p.rglob('*.py')\n",
"\n",
"dep_graph = {get_full_module_name(f): get_file_deps(f) for f in files} \n",
"dep_graph"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"reload_deps = {m: module_dependencies[m] for m in to_load}\n",
"print(reload_deps)\n",
"topo_sort(reload_deps)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"files = [\n",
" Path('/home/john/conf/apps/my_repo/src/my_pkg/my_sub_pkg/deep_pkg/__init__.py'),\n",
" Path('/home/john/conf/apps/my_repo/src/my_pkg/my_sub_pkg/deep_pkg/deep_mod.py'),\n",
"]\n",
"\n",
"for f in files:\n",
" mod_name = get_file_deps(f)\n",
" print(mod_name)\n",
" break\n",
"\n",
"with f.open(\"r\") as file:\n",
" file_content = file.read()\n",
"tree = ast.parse(file_content, filename=f)\n",
"for node in ast.walk(tree):\n",
" if isinstance(node, ast.ImportFrom):\n",
" print(node.level, node.module)\n",
" if node.level:\n",
" abs_module = resolve_relative_import(node, f)\n",
" if not node.module:\n",
" abs_module += f'.{node.names[0].name}'\n",
" print(abs_module)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## File Checking"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"files = app_dir.rglob('*.py')\n",
"\n",
"fc = FileCheck.from_paths(files)\n",
"print('\\n'.join(f'{f.relative_to(app_dir.parent)}' for f in fc.new))"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "appdaemon",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.8"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,6 @@
# print(f' Importing {__name__} '.center(50, '='))
from .foo import Foo
from .my_sub_pkg.bar import Bar
# print(' Done '.center(50, '='))

View File

@@ -0,0 +1,12 @@
from pathlib import Path
from appdaemon.adbase import ADBase
class Foo(ADBase):
changes: bool = True
def initialize(self):
self.adapi = self.get_ad_api()
self.adapi.log(f'Initialized app from {Path(__file__).relative_to(self.AD.app_dir.parent)}')
self.adapi.log(f'new line changes: {self.changes}')

View File

@@ -0,0 +1,5 @@
# print(f' Importing {__name__} '.center(50, '+'))
from .bar import Bar
# print(f' Done {__name__} '.center(50, '+'))

View File

@@ -0,0 +1,12 @@
from pathlib import Path
from appdaemon.adbase import ADBase
filename = Path(__file__).name
# print(f' Importing {filename} '.center(50, '-'))
class Bar(ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
self.log = self.adapi.log
self.log(f'Initialized app from {filename}')

View File

@@ -0,0 +1,16 @@
from pathlib import Path
from appdaemon.adbase import ADBase
# from . import deep_pkg
filename = Path(__file__).name
# print(f' Importing {filename} '.center(50, '-'))
class Baz(ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
self.adapi.log(f'Initialized app from {filename}')
# self.adapi.log(f'Initialized app from {filename}')
# self.adapi.log(f'CHANGED')

View File

@@ -0,0 +1,7 @@
from ...foo import Foo
from ..bar import Bar
from . import deep_mod
from .. import bar
from .deep_mod import Deep
# print(Deep.version)

View File

@@ -0,0 +1,13 @@
from pathlib import Path
from appdaemon.adbase import ADBase
filename = Path(__file__).name
class Deep(ADBase):
version = 'change'
def initialize(self):
self.adapi = self.get_ad_api()
self.adapi.log(f'Initialized app from {filename}')
# self.adapi.log(f'CHANGED')

11
apps/repo_apps.yaml Normal file
View File

@@ -0,0 +1,11 @@
Repo-Foo:
module: my_pkg
class: Foo
Repo-Bar:
module: my_pkg.my_sub_pkg
class: Bar
Repo-Baz:
module: my_pkg.my_sub_pkg.baz
class: Baz

4
apps/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
rich
anyio
pydantic
# arrow

18
apps/sequence.yaml Normal file
View File

@@ -0,0 +1,18 @@
sequence:
office_on:
name: Office On
namespace: hass
steps:
- homeassistant/turn_on:
entity_id: light.office_1
brightness: 254
- homeassistant/turn_on:
entity_id: light.office_2
brightness: 254
office_off:
name: Office Off
steps:
- homeassistant/turn_off:
entity_id: light.office_1
- homeassistant/turn_off:
entity_id: light.office_2

View File

@@ -0,0 +1,11 @@
Hello:
module: hello
class: HelloWorld
Hello2:
module: hello
class: HelloWorld
TestApp:
module: test
class: Test

11
apps/test/test.yaml Normal file
View File

@@ -0,0 +1,11 @@
test_notifier:
# module: test_notify
# class: TestNotifier
module: variant
class: Variant
log_level: DEBUG
# dependencies: companion_app
TestApp:
module: test_notify
class: TestNotifier

11
apps/test/test_notify.py Normal file
View File

@@ -0,0 +1,11 @@
from app1.notification import Notification
from appdaemon.adbase import ADBase
class TestNotifier(ADBase):
def initialize(self):
self.adapi = self.get_ad_api()
self.log = self.adapi.log
self.log(f'Initialized test notifier class: {self.__class__.__name__}')
self.notify = Notification(self.adapi, self.name)
self.notify.send('Some notifications')

8
apps/test/variant.py Normal file
View File

@@ -0,0 +1,8 @@
from test_notify import TestNotifier
from appdaemon.adapi import ADAPI
class Variant(TestNotifier, ADAPI):
def initialize(self):
super().initialize()
self.log('Variant of the TestNotifier')
# self.log(' VARIANT '.center(50, '*'))

View File

@@ -1,8 +1,8 @@
version: '3.9'
services: services:
appdaemon: appdaemon:
container_name: appdaemon_dev container_name: appdaemon_dev
image: appdaemon:jsl-dev image: appdaemon:local-dev
# image: acockburn/appdaemon:dev
volumes: volumes:
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro