Skip to content

Commit

Permalink
Refactor out a SmartAppConfigManager that can be overridden
Browse files Browse the repository at this point in the history
  • Loading branch information
pronovic committed Jun 24, 2022
1 parent 3e95cf9 commit 584669e
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 33 deletions.
4 changes: 4 additions & 0 deletions Changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Version 0.5.0 unreleased

* Refactor out a SmartAppConfigManager that can be overridden.

Version 0.4.1 19 Jun 2022

* Updates to published documentation.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "smartapp-sdk"
version = "0.4.1"
version = "0.5.0a1"
description = "Framework to build a webhook-based SmartThings SmartApp"
authors = ["Kenneth J. Pronovici <pronovic@ieee.org>"]
license = "Apache-2.0"
Expand Down
69 changes: 38 additions & 31 deletions src/smartapp/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
from .interface import (
AbstractRequest,
BadRequestError,
ConfigInit,
ConfigInitData,
ConfigPage,
ConfigPageData,
ConfigPhase,
ConfigurationInitResponse,
ConfigurationPageResponse,
Expand All @@ -33,6 +29,7 @@
LifecycleResponse,
OauthCallbackRequest,
OauthCallbackResponse,
SmartAppConfigManager,
SmartAppDefinition,
SmartAppDispatcherConfig,
SmartAppError,
Expand All @@ -46,6 +43,40 @@
from .signature import SignatureVerifier


@frozen(kw_only=True)
class StaticConfigManager(SmartAppConfigManager):

"""
Configuration manager that operates on static data.
This is the configuration manager used by default in the dispatcher. It operates on
a static set of config pages. This sort of static definition is adequate for lots of
SmartApps, but it doesn't work for some types of complex configuration, where the responses
need to be generated dynamically. In that case, you can implement your own configuration
manager with that specialized behavior.
"""

def handle_page(self, definition: SmartAppDefinition, page_id: int) -> ConfigurationPageResponse:
"""Handle a CONFIGURATION PAGE lifecycle request."""
if not definition.config_pages:
raise ValueError("Static configuration manager requires at least one configured page.")
previous_page_id = None if page_id == 1 else page_id - 1
next_page_id = None if page_id >= len(definition.config_pages) else page_id + 1
complete = page_id >= len(definition.config_pages)
try:
page = definition.config_pages[page_id - 1] # page_id is 1-based, but we need 0-based for array
return self.build_page_response(
name=page.page_name,
page_id=page_id,
previous_page_id=previous_page_id,
next_page_id=next_page_id,
complete=complete,
sections=page.sections,
)
except IndexError as e:
raise ValueError("Page not found: %d" % page_id) from e


@frozen(kw_only=True)
class SmartAppDispatcher:

Expand All @@ -65,6 +96,7 @@ class SmartAppDispatcher:
definition: SmartAppDefinition
event_handler: SmartAppEventHandler
config: SmartAppDispatcherConfig = field(factory=SmartAppDispatcherConfig)
manager: SmartAppConfigManager = field(factory=StaticConfigManager)

def dispatch(self, context: SmartAppRequestContext) -> str:
"""
Expand Down Expand Up @@ -134,32 +166,7 @@ def _handle_confirmation_request(self, request: ConfirmationRequest) -> Confirma
def _handle_config_request(self, request: ConfigurationRequest) -> Union[ConfigurationInitResponse, ConfigurationPageResponse]:
"""Handle a CONFIGURATION lifecycle request, returning an appropriate response."""
if request.configuration_data.phase == ConfigPhase.INITIALIZE:
return ConfigurationInitResponse(
configuration_data=ConfigInitData(
initialize=ConfigInit(
id=self.definition.id,
name=self.definition.name,
description=self.definition.description,
permissions=self.definition.permissions,
first_page_id="1",
)
)
)
return self.manager.handle_initialize(self.definition)
else: # if request.configuration_data.phase == ConfigPhase.PAGE:
page_id = int(request.configuration_data.page_id)
previous_page_id = None if page_id == 1 else str(page_id - 1)
next_page_id = None if page_id >= len(self.definition.config_pages) else str(page_id + 1)
complete = page_id >= len(self.definition.config_pages)
pages = self.definition.config_pages[page_id - 1] # page_id is 1-based
return ConfigurationPageResponse(
configuration_data=ConfigPageData(
page=ConfigPage(
name=pages.page_name,
page_id=request.configuration_data.page_id,
previous_page_id=previous_page_id,
next_page_id=next_page_id,
complete=complete,
sections=pages.sections,
)
)
)
return self.manager.handle_page(self.definition, page_id)
72 changes: 71 additions & 1 deletion src/smartapp/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,77 @@ class SmartAppDefinition:
description: str
target_url: str
permissions: List[str]
config_pages: List[SmartAppConfigPage]
config_pages: Optional[List[SmartAppConfigPage]]


# pylint: disable=redefined-builtin:
# noinspection PyShadowingBuiltins,PyMethodMayBeStatic
class SmartAppConfigManager(ABC):
"""
Configuration manager, used by the dispatcher to respond to CONFIGURATION events.
The dispatcher has a default configuration manager. However, you can implement your
own if that default behavior does not meet your needs. For instance, a static config
definition is adequate for lots of SmartApps, but it doesn't work for some types of
complex configuration, where the responses need to be generated dynamically. In that
case, you can implement your own configuration manager with that specialized behavior.
This abstract class also includes several convenience methods to make it easier to
build responses.
"""

def handle_initialize(self, definition: SmartAppDefinition) -> ConfigurationInitResponse:
"""Handle a CONFIGURATION INITIALIZE lifecycle request."""
return self.build_init_response(
id=definition.id,
name=definition.name,
description=definition.description,
permissions=definition.permissions,
first_page_id=1,
)

@abstractmethod
def handle_page(self, definition: SmartAppDefinition, page_id: int) -> ConfigurationPageResponse:
"""Handle a CONFIGURATION PAGE lifecycle request."""

def build_init_response(
self, id: str, name: str, description: str, permissions: List[str], first_page_id: int
) -> ConfigurationInitResponse:
"""Build a ConfigurationInitResponse."""
return ConfigurationInitResponse(
configuration_data=ConfigInitData(
initialize=ConfigInit(
id=id,
name=name,
description=description,
permissions=permissions,
first_page_id=str(first_page_id),
)
)
)

def build_page_response(
self,
page_id: int,
name: str,
previous_page_id: Optional[int],
next_page_id: Optional[int],
complete: bool,
sections: List[ConfigSection],
) -> ConfigurationPageResponse:
"""Build a ConfigurationPageResponse."""
return ConfigurationPageResponse(
configuration_data=ConfigPageData(
page=ConfigPage(
name=name,
page_id=str(page_id),
previous_page_id=str(previous_page_id) if previous_page_id else None,
next_page_id=str(next_page_id) if next_page_id else None,
complete=complete,
sections=sections,
)
)
)


# noinspection PyUnresolvedReferences
Expand Down

0 comments on commit 584669e

Please sign in to comment.