hassette


Namehassette JSON
Version 0.3.2 PyPI version JSON
download
home_pageNone
SummaryHassette is a modern, asynchronous, fully typed Python framework for running Home Assistant scripts and automations.
upload_time2025-09-08 04:07:23
maintainerNone
docs_urlNone
authorJessica
requires_python<3.14,>=3.11
licenseNone
keywords home-assistant automation async typed framework smart-home iot
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Hassette

[![PyPI version](https://badge.fury.io/py/hassette.svg)](https://badge.fury.io/py/hassette)
[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A modern, async-first Python framework for building Home Assistant automations with type safety and developer experience in mind.

Hassette brings the developer experience of modern Python projects to Home Assistant automation development. Think of it as an alternative to AppDaemon, built with today's Python tooling and best practices.

## โœจ Key Features

- **๐ŸŒŸ Async-first**: Built on `asyncio` with proper async/await patterns
- **๐Ÿ”ง Type-safe**: Full typing support for entities, events, and configurations
- **โšก Event-driven**: Powerful event bus with flexible filtering and routing
- **โฐ Flexible scheduling**: Custom scheduler with cron and interval support
- **๐ŸŽฏ Simple configuration**: TOML-based app configuration with Pydantic validation
- **๐Ÿงช Tested**: Core and utilities are tested with unit and integration tests, user testing framework is coming soon!

## ๐Ÿš€ Quick Start

### Installation

```bash
pip install hassette
```

### Basic Example

Create a simple battery monitoring app:

```python
from hassette import App, AppConfig

class BatteryConfig(AppConfig):
    threshold: float = 20
    notify_entity: str = "my_mobile_phone"

class BatteryMonitor(App[BatteryConfig]):
    async def initialize(self):
        # Run battery check every morning at 9 AM
        self.scheduler.run_cron(self.check_batteries, hour=9)

    async def check_batteries(self):
        states = await self.api.get_states()
        low_batteries = []

        for device in states:
            if hasattr(device.attributes, 'battery_level'):
                level = device.attributes.battery_level
                if level and level < self.app_config.threshold:
                    low_batteries.append(f"{device.entity_id}: {level}%")

        if low_batteries:
            message = "Low battery devices: " + ",".join(low_batteries)
            await self.api.call_service("notify", self.app_config.notify_entity, message=message)
```

### Configuration

Create `hassette.toml`:

```toml
[apps.battery_monitor]
enabled = true
app_path = "battery_monitor.py"
class_name = "BatteryMonitor"

[apps.battery_monitor.config]
threshold = 15
notify_entity = "notify.mobile_app_phone"
```

### Running

```bash
run-hassette
```

## ๐Ÿ“š Core Concepts

### Apps

Apps are the building blocks of your automations. They can:
- ๐Ÿ‘‚ Listen to Home Assistant events and state changes
- โฐ Schedule recurring tasks
- ๐Ÿ“ž Call Home Assistant services
- ๐Ÿ’พ Maintain their own state and configuration

### Configuration

Apps are configured via a `hassette.toml` file using Pydantic models for validation. Custom configuration is optional but recommended for most use cases.

**Configuration file (`hassette.toml`):**
```toml
[apps.my_app]  # Validated by AppManifest during Hassette startup
enabled = true
app_path = "my_app.py"
class_name = "MyApp"

[apps.my_app.config]  # Validated by your Pydantic class when MyApp initializes
entity_id = "light.living_room"
brightness_when_home = 200
brightness_when_away = 50
```

**App with typed configuration:**
```python
from hassette import App, AppConfig
from pydantic import Field

class MyAppConfig(AppConfig):
    entity_id: str = Field(..., description="Entity ID of the light")
    brightness_when_home: int = Field(200, ge=0, le=255)
    brightness_when_away: int = Field(50, ge=0, le=255)

class MyApp(App[MyAppConfig]):
    async def initialize(self):
        # Fully typed access to configuration
        light_id = self.app_config.entity_id
        brightness = self.app_config.brightness_when_home
```

### Event Handling

Subscribe to state changes with powerful filtering options:

```python
# Listen to all light state changes
self.bus.on_entity("light.*", handler=self.light_changed)

# Listen to specific state transitions
self.bus.on_entity("binary_sensor.motion", handler=self.motion_detected, changed_to="on")

# Listen to specific attribute changes
self.bus.on_attribute("mobile_device.my_phone", "battery_level", self.battery_level_changed)

# Listen to Hassette events (including your own apps' events)
self.bus.on_hassette_service_started(handler=self.my_app_started)
```

**Advanced filtering with complex conditions:**
```python
from hassette import predicates

# Create specific attribute change filters
light_color_changed = predicates.AttrChanged("color", to="alice_blue")
brightness_changed = predicates.AttrChanged("brightness", from_=0)  # leaving 'to' undefined means "any value"

# Combine multiple conditions
self.bus.on_entity("light.*", handler=self.light_changed,
                   where=predicates.AllOf(brightness_changed, light_color_changed))
```

### Scheduling

Schedule tasks with cron expressions or intervals using [`whenever`](https://github.com/ariebovenberg/whenever):

```python
# Every day at 6 AM
self.scheduler.run_cron(self.morning_routine, hour=6)

# Every 30 seconds
from whenever import TimeDelta
self.scheduler.run_every(self.check_sensors, TimeDelta(seconds=30))
# or simply:
self.scheduler.run_every(self.check_sensors, interval=30)

# One-time delayed execution
self.scheduler.run_in(self.delayed_task, delay=60*5)  # In 5 minutes
```

### Type Safety

Hassette provides comprehensive typing for Home Assistant entities and events:

```python
async def handle_light_change(self, event: StateChangeEvent[LightState]):
    # Event structure: event โ†’ payload โ†’ data
    # - event: Contains payload and topic (used for bus filtering)
    # - payload: Either HassPayload or HassettePayload with event_type and data
    # - data: The actual state change information

    light = event.payload.data
    if light.new_state_value == "on":
        brightness = light.new_state.attributes.brightness
        self.logger.info(f"Light turned on with brightness {brightness}")

    # Type information available at every level:
    reveal_type(light)                           # StateChangePayload[LightState]
    reveal_type(light.new_state)                 # LightState
    reveal_type(light.new_state.value)           # str (the "state" value)
    reveal_type(light.new_state.attributes.color_temp_kelvin)  # float
```

## ๐Ÿ“ Project Structure

```
your_project/
โ”œโ”€โ”€ hassette.toml          # Configuration
โ”œโ”€โ”€ apps/
โ”‚   โ”œโ”€โ”€ battery_monitor.py
โ”‚   โ”œโ”€โ”€ presence.py
โ”‚   โ””โ”€โ”€ climate_control.py
โ””โ”€โ”€ .env                   # Environment variables (optional)
```

## ๐Ÿ”ง Advanced Features

### Multiple App Instances

Configure multiple instances of the same app with different configurations:

```toml
[apps.presence]
enabled = true
app_path = "presence.py"
class_name = "PresenceApp"

# Multiple instances using [[apps.presence.config]]
[[apps.presence.config]]
name = "upstairs"
motion_sensor = "binary_sensor.upstairs_motion"
lights = ["light.bedroom", "light.hallway"]

[[apps.presence.config]]
name = "downstairs"
motion_sensor = "binary_sensor.downstairs_motion"
lights = ["light.living_room", "light.kitchen"]
```

**Single instance configuration:**
```toml
[apps.presence.config]  # Note: single [config] instead of [[config]]
name = "main"
motion_sensor = "binary_sensor.main_motion"
lights = ["light.living_room", "light.kitchen"]
```


### Synchronous Apps

For simpler synchronous use cases, use `AppSync`. Only a few changes are required:

1. Inherit from `AppSync` instead of `App`.
2. Implement `initialize_sync` instead of `initialize`.
3. Use the `.sync` API for Home Assistant calls.

```python
from hassette import AppSync

class SimpleApp(AppSync[AppConfig]):
    def initialize_sync(self):
        # scheduler and bus are available in sync apps too with no changes required
        self.scheduler.run_in(self.check_batteries, 10)
        self.bus.on_entity("*", handler=self.handle_sensor_event)

    def my_task(self):
        # Use .sync API for synchronous Home Assistant calls
        states = self.api.sync.get_states()
        # All async API methods have sync equivalents
```


## ๐Ÿ“– Examples

Check out the [`examples/`](examples/) directory for more complete examples:
- [Battery monitoring](examples/battery.py)
- [Presence detection](examples/presence.py)
- [Sensor notifications](examples/sensor_notification.py)

## ๐Ÿ›ฃ๏ธ Status & Roadmap

Hassette is brand new and under active development. We follow semantic versioning and recommend pinning a minor version while the API stabilizes.

### Current Focus Areas

- ๐Ÿ“š **Comprehensive documentation**
- ๐Ÿ” **Enhanced type safety**: Service calls/responses, additional state types
- ๐Ÿ—๏ธ **Entity classes**: Include state data and service functionality (e.g. `LightEntity.turn_on()`)
- ๐Ÿ”„ **Enhanced error handling**: Better retry logic and error recovery
- ๐Ÿงช **Testing improvements**:
  - ๐Ÿ“Š More tests for core and utilities
  - ๐Ÿ› ๏ธ Test fixtures and framework for user apps
  - ๐Ÿšซ No more manual state changes in HA Developer Tools for testing!
- ๐Ÿณ **Docker deployment support**

See the full [roadmap](roadmap.md) for details - open an issue or PR if you'd like to contribute or provide feedback!

## ๐Ÿค Contributing

Hassette is in active development and contributions are welcome! Whether you're:

- ๐Ÿ› Reporting bugs
- ๐Ÿ’ก Suggesting features
- ๐Ÿ“ Improving documentation
- ๐Ÿ”ง Contributing code

Early feedback and contributions help shape the project's direction.

## ๐Ÿ“„ License

[MIT](LICENSE)

---

**Note**: Hassette requires Python 3.11+ and a running Home Assistant instance with WebSocket API access.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "hassette",
    "maintainer": null,
    "docs_url": null,
    "requires_python": "<3.14,>=3.11",
    "maintainer_email": null,
    "keywords": "home-assistant, automation, async, typed, framework, smart-home, iot",
    "author": "Jessica",
    "author_email": "Jessica <12jessicasmith34@gmail.com>",
    "download_url": "https://files.pythonhosted.org/packages/1f/db/5401665ba3678d9f6bd2b51ef94332ba244ca6ccd436b8a23a823ca35ed9/hassette-0.3.2.tar.gz",
    "platform": null,
    "description": "# Hassette\n\n[![PyPI version](https://badge.fury.io/py/hassette.svg)](https://badge.fury.io/py/hassette)\n[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA modern, async-first Python framework for building Home Assistant automations with type safety and developer experience in mind.\n\nHassette brings the developer experience of modern Python projects to Home Assistant automation development. Think of it as an alternative to AppDaemon, built with today's Python tooling and best practices.\n\n## \u2728 Key Features\n\n- **\ud83c\udf1f Async-first**: Built on `asyncio` with proper async/await patterns\n- **\ud83d\udd27 Type-safe**: Full typing support for entities, events, and configurations\n- **\u26a1 Event-driven**: Powerful event bus with flexible filtering and routing\n- **\u23f0 Flexible scheduling**: Custom scheduler with cron and interval support\n- **\ud83c\udfaf Simple configuration**: TOML-based app configuration with Pydantic validation\n- **\ud83e\uddea Tested**: Core and utilities are tested with unit and integration tests, user testing framework is coming soon!\n\n## \ud83d\ude80 Quick Start\n\n### Installation\n\n```bash\npip install hassette\n```\n\n### Basic Example\n\nCreate a simple battery monitoring app:\n\n```python\nfrom hassette import App, AppConfig\n\nclass BatteryConfig(AppConfig):\n    threshold: float = 20\n    notify_entity: str = \"my_mobile_phone\"\n\nclass BatteryMonitor(App[BatteryConfig]):\n    async def initialize(self):\n        # Run battery check every morning at 9 AM\n        self.scheduler.run_cron(self.check_batteries, hour=9)\n\n    async def check_batteries(self):\n        states = await self.api.get_states()\n        low_batteries = []\n\n        for device in states:\n            if hasattr(device.attributes, 'battery_level'):\n                level = device.attributes.battery_level\n                if level and level < self.app_config.threshold:\n                    low_batteries.append(f\"{device.entity_id}: {level}%\")\n\n        if low_batteries:\n            message = \"Low battery devices: \" + \",\".join(low_batteries)\n            await self.api.call_service(\"notify\", self.app_config.notify_entity, message=message)\n```\n\n### Configuration\n\nCreate `hassette.toml`:\n\n```toml\n[apps.battery_monitor]\nenabled = true\napp_path = \"battery_monitor.py\"\nclass_name = \"BatteryMonitor\"\n\n[apps.battery_monitor.config]\nthreshold = 15\nnotify_entity = \"notify.mobile_app_phone\"\n```\n\n### Running\n\n```bash\nrun-hassette\n```\n\n## \ud83d\udcda Core Concepts\n\n### Apps\n\nApps are the building blocks of your automations. They can:\n- \ud83d\udc42 Listen to Home Assistant events and state changes\n- \u23f0 Schedule recurring tasks\n- \ud83d\udcde Call Home Assistant services\n- \ud83d\udcbe Maintain their own state and configuration\n\n### Configuration\n\nApps are configured via a `hassette.toml` file using Pydantic models for validation. Custom configuration is optional but recommended for most use cases.\n\n**Configuration file (`hassette.toml`):**\n```toml\n[apps.my_app]  # Validated by AppManifest during Hassette startup\nenabled = true\napp_path = \"my_app.py\"\nclass_name = \"MyApp\"\n\n[apps.my_app.config]  # Validated by your Pydantic class when MyApp initializes\nentity_id = \"light.living_room\"\nbrightness_when_home = 200\nbrightness_when_away = 50\n```\n\n**App with typed configuration:**\n```python\nfrom hassette import App, AppConfig\nfrom pydantic import Field\n\nclass MyAppConfig(AppConfig):\n    entity_id: str = Field(..., description=\"Entity ID of the light\")\n    brightness_when_home: int = Field(200, ge=0, le=255)\n    brightness_when_away: int = Field(50, ge=0, le=255)\n\nclass MyApp(App[MyAppConfig]):\n    async def initialize(self):\n        # Fully typed access to configuration\n        light_id = self.app_config.entity_id\n        brightness = self.app_config.brightness_when_home\n```\n\n### Event Handling\n\nSubscribe to state changes with powerful filtering options:\n\n```python\n# Listen to all light state changes\nself.bus.on_entity(\"light.*\", handler=self.light_changed)\n\n# Listen to specific state transitions\nself.bus.on_entity(\"binary_sensor.motion\", handler=self.motion_detected, changed_to=\"on\")\n\n# Listen to specific attribute changes\nself.bus.on_attribute(\"mobile_device.my_phone\", \"battery_level\", self.battery_level_changed)\n\n# Listen to Hassette events (including your own apps' events)\nself.bus.on_hassette_service_started(handler=self.my_app_started)\n```\n\n**Advanced filtering with complex conditions:**\n```python\nfrom hassette import predicates\n\n# Create specific attribute change filters\nlight_color_changed = predicates.AttrChanged(\"color\", to=\"alice_blue\")\nbrightness_changed = predicates.AttrChanged(\"brightness\", from_=0)  # leaving 'to' undefined means \"any value\"\n\n# Combine multiple conditions\nself.bus.on_entity(\"light.*\", handler=self.light_changed,\n                   where=predicates.AllOf(brightness_changed, light_color_changed))\n```\n\n### Scheduling\n\nSchedule tasks with cron expressions or intervals using [`whenever`](https://github.com/ariebovenberg/whenever):\n\n```python\n# Every day at 6 AM\nself.scheduler.run_cron(self.morning_routine, hour=6)\n\n# Every 30 seconds\nfrom whenever import TimeDelta\nself.scheduler.run_every(self.check_sensors, TimeDelta(seconds=30))\n# or simply:\nself.scheduler.run_every(self.check_sensors, interval=30)\n\n# One-time delayed execution\nself.scheduler.run_in(self.delayed_task, delay=60*5)  # In 5 minutes\n```\n\n### Type Safety\n\nHassette provides comprehensive typing for Home Assistant entities and events:\n\n```python\nasync def handle_light_change(self, event: StateChangeEvent[LightState]):\n    # Event structure: event \u2192 payload \u2192 data\n    # - event: Contains payload and topic (used for bus filtering)\n    # - payload: Either HassPayload or HassettePayload with event_type and data\n    # - data: The actual state change information\n\n    light = event.payload.data\n    if light.new_state_value == \"on\":\n        brightness = light.new_state.attributes.brightness\n        self.logger.info(f\"Light turned on with brightness {brightness}\")\n\n    # Type information available at every level:\n    reveal_type(light)                           # StateChangePayload[LightState]\n    reveal_type(light.new_state)                 # LightState\n    reveal_type(light.new_state.value)           # str (the \"state\" value)\n    reveal_type(light.new_state.attributes.color_temp_kelvin)  # float\n```\n\n## \ud83d\udcc1 Project Structure\n\n```\nyour_project/\n\u251c\u2500\u2500 hassette.toml          # Configuration\n\u251c\u2500\u2500 apps/\n\u2502   \u251c\u2500\u2500 battery_monitor.py\n\u2502   \u251c\u2500\u2500 presence.py\n\u2502   \u2514\u2500\u2500 climate_control.py\n\u2514\u2500\u2500 .env                   # Environment variables (optional)\n```\n\n## \ud83d\udd27 Advanced Features\n\n### Multiple App Instances\n\nConfigure multiple instances of the same app with different configurations:\n\n```toml\n[apps.presence]\nenabled = true\napp_path = \"presence.py\"\nclass_name = \"PresenceApp\"\n\n# Multiple instances using [[apps.presence.config]]\n[[apps.presence.config]]\nname = \"upstairs\"\nmotion_sensor = \"binary_sensor.upstairs_motion\"\nlights = [\"light.bedroom\", \"light.hallway\"]\n\n[[apps.presence.config]]\nname = \"downstairs\"\nmotion_sensor = \"binary_sensor.downstairs_motion\"\nlights = [\"light.living_room\", \"light.kitchen\"]\n```\n\n**Single instance configuration:**\n```toml\n[apps.presence.config]  # Note: single [config] instead of [[config]]\nname = \"main\"\nmotion_sensor = \"binary_sensor.main_motion\"\nlights = [\"light.living_room\", \"light.kitchen\"]\n```\n\n\n### Synchronous Apps\n\nFor simpler synchronous use cases, use `AppSync`. Only a few changes are required:\n\n1. Inherit from `AppSync` instead of `App`.\n2. Implement `initialize_sync` instead of `initialize`.\n3. Use the `.sync` API for Home Assistant calls.\n\n```python\nfrom hassette import AppSync\n\nclass SimpleApp(AppSync[AppConfig]):\n    def initialize_sync(self):\n        # scheduler and bus are available in sync apps too with no changes required\n        self.scheduler.run_in(self.check_batteries, 10)\n        self.bus.on_entity(\"*\", handler=self.handle_sensor_event)\n\n    def my_task(self):\n        # Use .sync API for synchronous Home Assistant calls\n        states = self.api.sync.get_states()\n        # All async API methods have sync equivalents\n```\n\n\n## \ud83d\udcd6 Examples\n\nCheck out the [`examples/`](examples/) directory for more complete examples:\n- [Battery monitoring](examples/battery.py)\n- [Presence detection](examples/presence.py)\n- [Sensor notifications](examples/sensor_notification.py)\n\n## \ud83d\udee3\ufe0f Status & Roadmap\n\nHassette is brand new and under active development. We follow semantic versioning and recommend pinning a minor version while the API stabilizes.\n\n### Current Focus Areas\n\n- \ud83d\udcda **Comprehensive documentation**\n- \ud83d\udd10 **Enhanced type safety**: Service calls/responses, additional state types\n- \ud83c\udfd7\ufe0f **Entity classes**: Include state data and service functionality (e.g. `LightEntity.turn_on()`)\n- \ud83d\udd04 **Enhanced error handling**: Better retry logic and error recovery\n- \ud83e\uddea **Testing improvements**:\n  - \ud83d\udcca More tests for core and utilities\n  - \ud83d\udee0\ufe0f Test fixtures and framework for user apps\n  - \ud83d\udeab No more manual state changes in HA Developer Tools for testing!\n- \ud83d\udc33 **Docker deployment support**\n\nSee the full [roadmap](roadmap.md) for details - open an issue or PR if you'd like to contribute or provide feedback!\n\n## \ud83e\udd1d Contributing\n\nHassette is in active development and contributions are welcome! Whether you're:\n\n- \ud83d\udc1b Reporting bugs\n- \ud83d\udca1 Suggesting features\n- \ud83d\udcdd Improving documentation\n- \ud83d\udd27 Contributing code\n\nEarly feedback and contributions help shape the project's direction.\n\n## \ud83d\udcc4 License\n\n[MIT](LICENSE)\n\n---\n\n**Note**: Hassette requires Python 3.11+ and a running Home Assistant instance with WebSocket API access.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Hassette is a modern, asynchronous, fully typed Python framework for running Home Assistant scripts and automations.",
    "version": "0.3.2",
    "project_urls": {
        "Bug Reports": "https://github.com/nodejsmith/hassette/issues",
        "Changelog": "https://github.com/nodejsmith/hassette/blob/main/CHANGELOG.md",
        "Documentation": "https://github.com/nodejsmith/hassette#readme",
        "Homepage": "https://github.com/nodejsmith/hassette",
        "Repository": "https://github.com/nodejsmith/hassette"
    },
    "split_keywords": [
        "home-assistant",
        " automation",
        " async",
        " typed",
        " framework",
        " smart-home",
        " iot"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "31699db4f35bf333dd20156a9fd8e59924ace45519450a393fb4e7cb0e646773",
                "md5": "4994e73cc1cbd34819c7c4a4cc715c26",
                "sha256": "01a6a1af65350cc23e0fce02d3c7787fb4d00eac34e0705667c9e15210757fc3"
            },
            "downloads": -1,
            "filename": "hassette-0.3.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "4994e73cc1cbd34819c7c4a4cc715c26",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": "<3.14,>=3.11",
            "size": 92946,
            "upload_time": "2025-09-08T04:07:21",
            "upload_time_iso_8601": "2025-09-08T04:07:21.419331Z",
            "url": "https://files.pythonhosted.org/packages/31/69/9db4f35bf333dd20156a9fd8e59924ace45519450a393fb4e7cb0e646773/hassette-0.3.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "1fdb5401665ba3678d9f6bd2b51ef94332ba244ca6ccd436b8a23a823ca35ed9",
                "md5": "ceecf8e01432d2f9ef6052bcd536eff3",
                "sha256": "27bb41a1d323ea0132d1b98af24d408062dc85cb33af873332301af7787c7db1"
            },
            "downloads": -1,
            "filename": "hassette-0.3.2.tar.gz",
            "has_sig": false,
            "md5_digest": "ceecf8e01432d2f9ef6052bcd536eff3",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": "<3.14,>=3.11",
            "size": 61766,
            "upload_time": "2025-09-08T04:07:23",
            "upload_time_iso_8601": "2025-09-08T04:07:23.267112Z",
            "url": "https://files.pythonhosted.org/packages/1f/db/5401665ba3678d9f6bd2b51ef94332ba244ca6ccd436b8a23a823ca35ed9/hassette-0.3.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-09-08 04:07:23",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "nodejsmith",
    "github_project": "hassette",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "hassette"
}
        
Elapsed time: 3.68195s