Skip to content

Commit 8bcdba6

Browse files
authored
Merge pull request #2452 from AppDaemon/python313
python 3.13 support
2 parents 3be1f42 + fb1b3ae commit 8bcdba6

File tree

20 files changed

+628
-282
lines changed

20 files changed

+628
-282
lines changed

.github/workflows/python-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
strategy:
4545
fail-fast: false
4646
matrix:
47-
python-version: ["3.10", "3.11", "3.12"]
47+
python-version: ["3.10", "3.11", "3.12", "3.13"]
4848

4949
steps:
5050
- uses: actions/checkout@v5

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

appdaemon/adapi.py

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -587,99 +587,115 @@ async def get_pin_thread(self) -> int:
587587
# Namespace
588588
#
589589

590-
def set_namespace(self, namespace: str, writeback: str = "safe", persist: bool = True) -> None:
591-
"""Set the current namespace of the app
590+
def set_namespace(
591+
self,
592+
namespace: str,
593+
writeback: Literal["safe", "hybrid"] | utils.ADWritebackType = "safe",
594+
persist: bool = True,
595+
) -> None:
596+
"""Set the current namespace of the app.
597+
598+
This will create a new namespace if it doesn't already exist. By default, this will be a persistent namespace
599+
with ``safe`` writeback, which means that all state changes will be stored to disk as they happen.
592600
593-
See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
601+
See the :py:ref:`app_namespaces` for more information.
594602
595603
Args:
596604
namespace (str): Name of the new namespace
597605
writeback (str, optional): The writeback to be used if a new namespace gets created. Will be ``safe`` by
598606
default.
599607
persist (bool, optional): Whether to make the namespace persistent if a new one is created. Defaults to
600-
``True``.
608+
`True`.
601609
602610
Returns:
603611
None.
604612
605613
Examples:
606-
>>> self.set_namespace("hass1")
614+
Create a namespace that buffers state changes in memory and periodically writes them to disk.
607615
616+
>>> self.set_namespace("on_disk", writeback="hybrid", persist=True)
617+
618+
Create an in-memory namespace that won't survive AppDaemon restarts.
619+
620+
>>> self.set_namespace("in_memory", persist=False)
608621
"""
609-
# Keeping namespace get/set functions for legacy compatibility
610622
if not self.namespace_exists(namespace):
611-
self.add_namespace(namespace=namespace, writeback=writeback, persist=persist)
623+
self.add_namespace(
624+
namespace=namespace,
625+
writeback=utils.ADWritebackType(writeback),
626+
persist=persist
627+
)
612628
self.namespace = namespace
613629

614630
def get_namespace(self) -> str:
615631
"""Get the app's current namespace.
616632
617-
See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
633+
See :py:ref:`app_namespaces` for more information.
618634
"""
619635
# Keeping namespace get/set functions for legacy compatibility
620636
return self.namespace
621637

622638
@utils.sync_decorator
623639
async def namespace_exists(self, namespace: str) -> bool:
624-
"""Check the existence of a namespace in AppDaemon.
640+
"""Check for the existence of a namespace.
625641
626-
See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
642+
See :py:ref:`app_namespaces` for more information.
627643
628644
Args:
629-
namespace (str): The namespace to be checked.
645+
namespace (str): The namespace to check for.
630646
631647
Returns:
632-
bool: ``True`` if the namespace exists, otherwise ``False``.
633-
634-
Examples:
635-
Check if the namespace ``storage`` exists within AD
636-
637-
>>> if self.namespace_exists("storage"):
638-
>>> #do something like create it
639-
648+
bool: `True` if the namespace exists, otherwise `False`.
640649
"""
641650
return self.AD.state.namespace_exists(namespace)
642651

643652
@utils.sync_decorator
644-
async def add_namespace(self, namespace: str, writeback: str = "safe", persist: bool = True) -> str | None:
645-
"""Add a user-defined namespace, which has a database file associated with it.
646-
647-
When AppDaemon restarts these entities will be loaded into the namespace with all their previous states. This
648-
can be used as a basic form of non-volatile storage of entity data. Depending on the configuration of the
649-
namespace, this function can be setup to constantly be running automatically
650-
or only when AD shutdown.
653+
async def add_namespace(
654+
self,
655+
namespace: str,
656+
writeback: Literal["safe", "hybrid"] | utils.ADWritebackType = "safe",
657+
persist: bool = True,
658+
) -> str | None:
659+
"""Add a user-defined namespace.
651660
652-
See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
661+
See the :py:ref:`app_namespaces` for more information.
653662
654663
Args:
655664
namespace (str): The name of the new namespace to create
656-
writeback (optional): The writeback to be used. Will be ``safe`` by default
665+
writeback (optional): The writeback to be used. Defaults to ``safe``, which writes every state change to
666+
disk. This can be problematic for namespaces that have a lot of state changes. `Safe` in this case
667+
refers data loss, rather than performance. The other option is ``hybrid``, which buffers state changes.
657668
persist (bool, optional): Whether to make the namespace persistent. Persistent namespaces are stored in a
658-
database file and are reloaded when AppDaemon restarts. Defaults to ``True``
669+
database file and are reloaded when AppDaemon restarts. Defaults to `True`.
659670
660671
Returns:
661-
The file path to the newly created namespace. Will be ``None`` if not persistent
672+
The file path to the newly created namespace. Will be ``None`` if not persistent.
662673
663674
Examples:
664-
Add a new namespace called `storage`.
675+
Create a namespace that buffers state changes in memory and periodically writes them to disk.
676+
677+
>>> self.add_namespace("on_disk", writeback="hybrid", persist=True)
665678
666-
>>> self.add_namespace("storage")
679+
Create an in-memory namespace that won't survive AppDaemon restarts.
667680
681+
>>> self.add_namespace("in_memory", persist=False)
668682
"""
669-
new_namespace = await self.AD.state.add_namespace(namespace, writeback, persist, self.name)
670-
match new_namespace:
671-
case Path() | str():
672-
new_namespace = str(new_namespace)
673-
self.AD.state.app_added_namespaces.add(new_namespace)
674-
return new_namespace
675-
case _:
676-
self.logger.warning("Namespace %s already exists or was not created", namespace)
683+
match await self.AD.state.add_namespace(
684+
namespace,
685+
utils.ADWritebackType(writeback),
686+
persist,
687+
self.name
688+
):
689+
case Path() as ns_path:
690+
return str(ns_path)
691+
case False | None:
692+
return None
677693

678694
@utils.sync_decorator
679695
async def remove_namespace(self, namespace: str) -> dict[str, Any] | None:
680696
"""Remove a user-defined namespace, which has a database file associated with it.
681697
682-
See the `namespace documentation <APPGUIDE.html#namespaces>`__ for more information.
698+
See :py:ref:`app_namespaces` for more information.
683699
684700
Args:
685701
namespace (str): The namespace to be removed, which must not be the current namespace.
@@ -709,32 +725,22 @@ async def list_namespaces(self) -> list[str]:
709725
return self.AD.state.list_namespaces()
710726

711727
@utils.sync_decorator
712-
async def save_namespace(self, namespace: str | None = None) -> None:
713-
"""Saves entities created in user-defined namespaces into a file.
728+
async def save_namespace(self, namespace: str | None = None) -> bool:
729+
"""Saves the given state namespace to its corresponding file.
714730
715-
This way, when AD restarts these entities will be reloaded into AD with its
716-
previous states within the namespace. This can be used as a basic form of
717-
non-volatile storage of entity data. Depending on the configuration of the
718-
namespace, this function can be setup to constantly be running automatically
719-
or only when AD shutdown. This function also allows for users to manually
720-
execute the command as when needed.
731+
This is only relevant for persistent namespaces, which if not set to ``safe`` buffers changes in memory and only
732+
periodically writes them to disk. This function manually forces a write of all the changes since the last save
733+
to disk. See the :py:ref:`app_namespaces` docs section for more information.
721734
722735
Args:
723-
namespace (str, optional): Namespace to use for the call. See the section on
724-
`namespaces <APPGUIDE.html#namespaces>`__ for a detailed description.
725-
In most cases it is safe to ignore this parameter.
736+
namespace (str, optional): Namespace to save. If not specified, the current app namespace will be used.
726737
727738
Returns:
728-
None.
729-
730-
Examples:
731-
Save all entities of the default namespace.
732-
733-
>>> self.save_namespace()
739+
bool: `True` if the namespace was saved successfully, `False` otherwise.
734740
735741
"""
736742
namespace = namespace if namespace is not None else self.namespace
737-
await self.AD.state.save_namespace(namespace)
743+
return await self.AD.state.save_namespace(namespace)
738744

739745
#
740746
# Utility

appdaemon/appdaemon.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ def start(self) -> None:
376376
self.thread_async.start()
377377
self.sched.start()
378378
self.utility.start()
379+
self.state.start()
379380

380381
if self.apps_enabled:
381382
self.app_management.start()

appdaemon/models/config/misc.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import json
2-
from datetime import datetime
2+
from datetime import datetime, timedelta
33
from pathlib import Path
44
from typing import Any, Literal
55

66
from pydantic import BaseModel, Field, model_validator
77

8+
from appdaemon.utils import ADWritebackType
9+
10+
from .common import ParsedTimedelta
11+
812
LEVELS = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
913

1014

@@ -31,23 +35,24 @@ class FilterConfig(BaseModel):
3135

3236

3337
class NamespaceConfig(BaseModel):
34-
writeback: Literal["safe", "hybrid"] | None = None
38+
writeback: ADWritebackType | None = None
3539
persist: bool = Field(default=False, alias="persistent")
40+
save_interval: ParsedTimedelta = Field(default=timedelta(seconds=1))
3641

3742
@model_validator(mode="before")
3843
@classmethod
3944
def validate_persistence(cls, values: Any):
4045
"""Sets persistence to True if writeback is set to safe or hybrid."""
4146
match values:
4247
case {"writeback": wb} if wb is not None:
43-
values["persistent"] = True
48+
values["persist"] = True
4449
case _ if getattr(values, "writeback", None) is not None:
45-
values.persistent = True
50+
values.persist = True
4651
return values
4752

4853
@model_validator(mode="after")
4954
def validate_writeback(self):
5055
"""Makes the writeback safe by default if persist is set to True."""
5156
if self.persist and self.writeback is None:
52-
self.writeback = "safe"
57+
self.writeback = ADWritebackType.safe
5358
return self

0 commit comments

Comments
 (0)