Skip to content

Commit

Permalink
build: cleanup project and use pyproject.toml instead of setup.py (#6)
Browse files Browse the repository at this point in the history
- Remove legacy setup.py and only use pyproject.toml
- Allow patch updates in dependencies
- Set version information dynamically from Git tags with setuptools_scm and embedd in package
- Add a setup-flow example
  • Loading branch information
zehnm authored Nov 4, 2023
1 parent b2083c4 commit 84d2c1c
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 129 deletions.
3 changes: 1 addition & 2 deletions .github/workflows/python-code-format.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ on:
- 'test-requirements.txt'
- 'tests/**'
- '.github/**/*.yml'
- '.pylintrc'
- 'pyproject.toml'
- 'tox.ini'
- '*.cfg'
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,4 @@ ENV/
.vscode/

.DS_Store
/ucapi/_version.py
29 changes: 0 additions & 29 deletions .pylintrc

This file was deleted.

19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
This library simplifies writing Python based integrations for the [Unfolded Circle Remote Two](https://www.unfoldedcircle.com/)
by wrapping the [WebSocket Integration API](https://github.com/unfoldedcircle/core-api/tree/main/integration-api).

It's a pre-alpha release (in our eyes). Missing features will be added continuously.
It's an alpha release (in our eyes). Breaking changes are to be expected and missing features will be continuously added.
Based on our [Node.js integration library](https://github.com/unfoldedcircle/integration-node-library).

❗️**Attention:**
Expand All @@ -21,9 +21,22 @@ Requirements:

## Usage

See [examples directory](examples) for a minimal integration driver example.
Install build tools:
```shell
pip3 install build setuptools setuptools_scm
```

More examples will be published.
Build:
```shell
python -m build
```

Local installation:
```shell
pip3 install --force-reinstall dist/ucapi-$VERSION-py3-none-any.whl
```

See [examples directory](examples) for a minimal integration driver example. More examples will be published.

### Environment Variables

Expand Down
5 changes: 4 additions & 1 deletion docs/code_guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ following customization:
Install all code linting tools:

```shell
pip3 install -r test-requirements.txt
pip3 install -r requirements.txt -r test-requirements.txt
```

Note: once <https://github.com/pypa/pip/issues/11440> is implemented, the requirements files can be removed and
`pyproject.toml` is sufficient.

### Linting

```shell
Expand Down
11 changes: 0 additions & 11 deletions docs/setup.md

This file was deleted.

35 changes: 35 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# API wrapper examples

This directory contains a few examples on how to use the Remote Two Integration-API wrapper.

Each example uses a driver metadata definition file. It's a json file named after the example.
The most important fields are:

- `driver_id`: unique identifier of the driver. Make sure you create a new ID for every driver.
- `port` defines the listening port of the WebSocket server for the Remote Two to connect to.
- This port is published in the mDNS service information.
- `name`: Friendly name of the driver to show.

See the [WebSocket Integration API documentation](https://github.com/unfoldedcircle/core-api/tree/main/doc/integration-driver)

## hello_integration

The [hello_integration.py](hello_integration.py) example is a "hello world" example showing the bare minimum required
to start with an integration driver for the Remote Two.

It defines a single push button with a callback handler. When pushed, it just prints a message in the console.

## setup_flow

The [setup_flow](setup_flow.py) example shows how to define a dynamic setup flow for the driver setup.

If the user selects the _expert_ option in the main setup screen:
1. An input screen is shown asking to select an item from a dropdown list.
2. The chosen option will be shown in the next input screen with another setting, on how many button entities to create.
3. The number of push buttons are created.

The available input settings are defined in the [Integration-API asyncapi.yaml definition](https://github.com/unfoldedcircle/core-api/tree/main/integration-api)
and are not yet available as typed Python objects.

See `Setting` object definition and the referenced SettingTypeNumber, SettingTypeText, SettingTypeTextArea,
SettingTypePassword, SettingTypeCheckbox, SettingTypeDropdown, SettingTypeLabel.
9 changes: 6 additions & 3 deletions examples/hello_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any]
return ucapi.StatusCodes.OK


@api.listens_to(ucapi.Events.CONNECT)
async def on_connect() -> None:
"""When the remote connects, we just set the device state. We are ready all the time!"""
await api.set_device_state(ucapi.DeviceStates.CONNECTED)


if __name__ == "__main__":
logging.basicConfig()

Expand All @@ -36,8 +42,5 @@ async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any]
)
api.available_entities.add(button)

# We are ready all the time! Otherwise, use @api.listens_to(ucapi.Events.CONNECT) & DISCONNECT
api.set_device_state(ucapi.DeviceStates.CONNECTED)

loop.run_until_complete(api.init("hello_integration.json"))
loop.run_forever()
40 changes: 40 additions & 0 deletions examples/setup_flow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"driver_id": "setupflow_example",
"version": "0.0.1",
"min_core_api": "0.20.0",
"name": { "en": "Setup Flow Demo" },
"icon": "uc:integration",
"description": {
"en": "Setup Flow Python integration driver example."
},
"port": 9081,
"developer": {
"name": "Unfolded Circle ApS",
"email": "[email protected]",
"url": "https://www.unfoldedcircle.com"
},
"home_page": "https://www.unfoldedcircle.com",
"setup_data_schema": {
"title": {
"en": "Example settings",
"de": "Beispiel Konfiguration",
"fr": "Exemple de configuration"
},
"settings": [
{
"id": "expert",
"label": {
"en": "Configure enhanced options",
"de": "Erweiterte Optionen konfigurieren",
"fr": "Configurer les options avancées"
},
"field": {
"checkbox": {
"value": false
}
}
}
]
},
"release_date": "2023-11-03"
}
170 changes: 170 additions & 0 deletions examples/setup_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Integration setup flow example."""
import asyncio
import logging
from typing import Any

import ucapi

loop = asyncio.get_event_loop()
api = ucapi.IntegrationAPI(loop)


async def driver_setup_handler(msg: ucapi.SetupDriver) -> ucapi.SetupAction:
"""
Dispatch driver setup requests to corresponding handlers.
Either start the setup process or handle the provided user input data.
:param msg: the setup driver request object, either DriverSetupRequest or UserDataResponse
:return: the setup action on how to continue
"""
if isinstance(msg, ucapi.DriverSetupRequest):
return await handle_driver_setup(msg)
if isinstance(msg, ucapi.UserDataResponse):
return await handle_user_data_response(msg)

# user confirmation not used in our demo setup process
# if isinstance(msg, UserConfirmationResponse):
# return handle_user_confirmation(msg)

return ucapi.SetupError()


async def handle_driver_setup(msg: ucapi.DriverSetupRequest) -> ucapi.RequestUserInput | ucapi.SetupError:
"""
Start driver setup.
Initiated by Remote Two to set up the driver.
:param msg: not used, value(s) of input fields in the first setup screen. See setup_data_schema in driver metadata.
:return: the setup action on how to continue
"""
# for our demo we clear everything, a real driver might have to handle this differently
api.available_entities.clear()
api.configured_entities.clear()

# check if user selected the expert option in the initial setup screen
# please note that all values are returned as strings!
if "expert" not in msg.setup_data or msg.setup_data["expert"] != "true":
return ucapi.SetupComplete()

# Dropdown selections are usually set dynamically, e.g. with found devices etc.
dropdown_items = [
{"id": "red", "label": {"en": "Red", "de": "Rot"}},
{"id": "green", "label": {"en": "Green", "de": "Grün"}},
{"id": "blue", "label": {"en": "Blue", "de": "Blau"}},
]

return ucapi.RequestUserInput(
{"en": "Please choose", "de": "Bitte auswählen"},
[
{
"id": "info",
"label": {"en": "Setup flow example", "de": "Setup Flow Beispiel"},
"field": {
"label": {
"value": {
"en": "This is just some informational text.\n"
+ "Simple **Markdown** is supported!\n"
+ "For example _some italic text_.\n"
+ "## Or a header text\n~~strikethrough txt~~",
}
}
},
},
{
"field": {"dropdown": {"value": "", "items": dropdown_items}},
"id": "step1.choice",
"label": {
"en": "Choose color",
"de": "Wähle Farbe",
},
},
],
)


async def handle_user_data_response(msg: ucapi.UserDataResponse) -> ucapi.SetupAction:
"""
Process user data response in a setup process.
Driver setup callback to provide requested user data during the setup process.
:param msg: response data from the requested user data
:return: the setup action on how to continue: SetupComplete if finished.
"""
# values from all screens are returned: check in reverse order
if "step2.count" in msg.input_values:
for x in range(int(msg.input_values["step2.count"])):
button = ucapi.Button(
f"button{x}",
f"Button {x + 1}",
cmd_handler=cmd_handler,
)
api.available_entities.add(button)

return ucapi.SetupComplete()

if "step1.choice" in msg.input_values:
choice = msg.input_values["step1.choice"]
print(f"Chosen color: {choice}")
return ucapi.RequestUserInput(
{"en": "Step 2"},
[
{
"id": "info",
"label": {
"en": "Selected value from previous step:",
"de": "Selektierter Wert vom vorherigen Schritt:",
},
"field": {
"label": {
"value": {
"en": choice,
}
}
},
},
{
"field": {"number": {"value": 1, "min": 1, "max": 100, "steps": 2}},
"id": "step2.count",
"label": {
"en": "Button instance count",
"de": "Anzahl Button Instanzen",
},
},
],
)

print("No choice was received")
return ucapi.SetupError()


async def cmd_handler(entity: ucapi.Button, cmd_id: str, _params: dict[str, Any] | None) -> ucapi.StatusCodes:
"""
Push button command handler.
Called by the integration-API if a command is sent to a configured button-entity.
:param entity: button entity
:param cmd_id: command
:param _params: optional command parameters
:return: status of the command
"""
print(f"Got {entity.id} command request: {cmd_id}")

return ucapi.StatusCodes.OK


@api.listens_to(ucapi.Events.CONNECT)
async def on_connect() -> None:
"""When the remote connects, we just set the device state. We are ready all the time!"""
await api.set_device_state(ucapi.DeviceStates.CONNECTED)


if __name__ == "__main__":
logging.basicConfig()

loop.run_until_complete(api.init("setup_flow.json", driver_setup_handler))
loop.run_forever()
Loading

0 comments on commit 84d2c1c

Please sign in to comment.