asher-cli 1.4.1__tar.gz → 1.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.claude/skills/pylitterbot-ref/SKILL.md +1 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/workflows/bandit.yml +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/workflows/ci.yml +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/workflows/coverage.yml +1 -1
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/workflows/release.yml +9 -6
- {asher_cli-1.4.1 → asher_cli-1.6.0}/CHANGELOG.md +24 -4
- {asher_cli-1.4.1 → asher_cli-1.6.0}/CLAUDE.md +4 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/PKG-INFO +6 -7
- {asher_cli-1.4.1 → asher_cli-1.6.0}/README.md +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/export.py +4 -4
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/faults.py +3 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/helpers.py +5 -5
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/mcp/config.py +18 -9
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/robot.py +1 -1
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/slash.py +13 -8
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/history.py +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/CONTRIBUTING.md +1 -1
- {asher_cli-1.4.1 → asher_cli-1.6.0}/pyproject.toml +10 -10
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/conftest.py +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_activity_labels.py +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_app_pilot.py +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_commands_pilot.py +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_completion.py +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_export.py +5 -5
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_faults.py +5 -1
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_headless.py +4 -4
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_history_view.py +14 -16
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_lr5_commands.py +2 -2
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_mcp_command.py +9 -1
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_mcp_config.py +36 -6
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_missing_robot_commands.py +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_monitoring.py +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_new_commands_pilot.py +3 -3
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/testhelpers.py +11 -11
- {asher_cli-1.4.1 → asher_cli-1.6.0}/uv.lock +675 -1076
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.claude/hooks/block-env.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.claude/settings.json +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.claude/skills/mcp-bridge/SKILL.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.claude/skills/textual/SKILL.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.env.example +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.githooks/pre-push +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/FUNDING.yml +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/pull_request_template.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.github/workflows/dependency-automerge.yml +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.gitignore +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.vscode/launch.json +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.vscode/settings.json +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/.vscode/tasks.json +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/CODEOWNERS +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/LICENSE +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/app.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/__main__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/activity.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/config.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/constants.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/credentials.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/theme.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/core/updates.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/autostart.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/daemon.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/desktoptheme.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/launcher.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/notifications.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/tray.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/desktop/watcher.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/headless.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/mcp/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/mcp/bridge.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/robot/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/robot/adapters.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/robot/protocol.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/app.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/cats.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/base.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/builtin.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/lr5.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/commands/registry.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/completion.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/connection.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/loginflow.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/loginscreen.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/monitoring.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/style.tcss +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/asher/tui/ui.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/cliff.toml +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/ROADMAP.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/README.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/cat-panel-badges.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/config-persistence.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/credentials.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/desktop-notifications.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/e2e-pilot.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/fault-monitoring.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/headless-export.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/history-export.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/lr5-features.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/multi-pet.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/robot-commands.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/runtime-slash-commands.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/slash-command-spec.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/sleep-schedule.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/tab-completion.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/ui-ux.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/version-display.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/docs/roadmap-archive/websocket-updates.md +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/renovate.json +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/requirements.txt +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/__init__.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_auth.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_auth_pilot.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_autostart.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_cats.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_config.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_connection_mixin.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_credentials.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_daemon.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_desktoptheme.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_launcher.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_layering.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_mcp_bridge.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_notifications.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_robot_adapters.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_tray.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_ui.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_updates.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_version_command.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/tests/test_watcher.py +0 -0
- {asher_cli-1.4.1 → asher_cli-1.6.0}/watchrun.py +0 -0
|
@@ -20,6 +20,7 @@ robot.sleep_mode_enabled # bool — NOT "sleeping"
|
|
|
20
20
|
robot.panel_lock_enabled # bool — NOT "panel_lockout"
|
|
21
21
|
robot.night_light_mode_enabled # bool
|
|
22
22
|
robot.last_seen # datetime | None
|
|
23
|
+
robot.is_laser_dirty # bool — LR4 since 2025.7.0, LR5 always
|
|
23
24
|
```
|
|
24
25
|
|
|
25
26
|
## Robot methods (all async, await required)
|
|
@@ -31,17 +31,17 @@ jobs:
|
|
|
31
31
|
runs-on: ubuntu-latest
|
|
32
32
|
steps:
|
|
33
33
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
34
|
-
- uses: astral-sh/setup-uv@
|
|
34
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
35
35
|
|
|
36
36
|
- name: Bandit scan
|
|
37
37
|
# --exit-zero: findings must not fail the job, or the upload step below
|
|
38
38
|
# is skipped and code scanning keeps showing the previous run's alerts.
|
|
39
39
|
run: >-
|
|
40
|
-
uvx --from 'bandit[toml,sarif]' bandit
|
|
40
|
+
uvx --from 'bandit[toml,sarif]==1.9.4' bandit
|
|
41
41
|
-c pyproject.toml -r . -f sarif -o bandit.sarif --exit-zero
|
|
42
42
|
|
|
43
43
|
- name: Upload SARIF to code scanning
|
|
44
|
-
uses: github/codeql-action/upload-sarif@
|
|
44
|
+
uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
|
|
45
45
|
with:
|
|
46
46
|
sarif_file: bandit.sarif
|
|
47
47
|
category: bandit
|
|
@@ -19,7 +19,7 @@ jobs:
|
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
21
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
22
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
23
23
|
- run: uv sync --no-default-groups --group dev
|
|
24
24
|
- run: uv run ruff check .
|
|
25
25
|
- run: uv run ruff format --check .
|
|
@@ -30,11 +30,11 @@ jobs:
|
|
|
30
30
|
needs: lint
|
|
31
31
|
strategy:
|
|
32
32
|
matrix:
|
|
33
|
-
python-version: ["3.
|
|
33
|
+
python-version: ["3.11", "3.12"]
|
|
34
34
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
35
35
|
runs-on: ${{ matrix.os }}
|
|
36
36
|
steps:
|
|
37
37
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
38
|
-
- uses: astral-sh/setup-uv@
|
|
38
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
39
39
|
- run: uv sync --no-default-groups --group dev
|
|
40
40
|
- run: uv run pytest tests/ -v --tb=short
|
|
@@ -18,7 +18,7 @@ jobs:
|
|
|
18
18
|
runs-on: ubuntu-latest
|
|
19
19
|
steps:
|
|
20
20
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
21
|
-
- uses: astral-sh/setup-uv@
|
|
21
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
22
22
|
- run: uv sync --no-default-groups --group dev
|
|
23
23
|
- name: Run tests with coverage
|
|
24
24
|
run: uv run pytest tests/ --cov=asher --cov-report=lcov --cov-report=term-missing
|
|
@@ -6,8 +6,7 @@ on:
|
|
|
6
6
|
- "release/*"
|
|
7
7
|
|
|
8
8
|
permissions:
|
|
9
|
-
contents:
|
|
10
|
-
id-token: write # needed for PyPI OIDC trusted publishing
|
|
9
|
+
contents: read
|
|
11
10
|
|
|
12
11
|
jobs:
|
|
13
12
|
build:
|
|
@@ -20,9 +19,9 @@ jobs:
|
|
|
20
19
|
- name: Extract version from branch name
|
|
21
20
|
id: version
|
|
22
21
|
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
|
23
|
-
- uses: astral-sh/setup-uv@
|
|
22
|
+
- uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
|
|
24
23
|
- run: uv build
|
|
25
|
-
- uses: actions/upload-artifact@
|
|
24
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7
|
|
26
25
|
with:
|
|
27
26
|
name: dist
|
|
28
27
|
path: dist/
|
|
@@ -32,8 +31,10 @@ jobs:
|
|
|
32
31
|
needs: build
|
|
33
32
|
runs-on: ubuntu-latest
|
|
34
33
|
environment: pypi
|
|
34
|
+
permissions:
|
|
35
|
+
id-token: write # needed for PyPI OIDC trusted publishing
|
|
35
36
|
steps:
|
|
36
|
-
- uses: actions/download-artifact@
|
|
37
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
37
38
|
with:
|
|
38
39
|
name: dist
|
|
39
40
|
path: dist/
|
|
@@ -43,9 +44,11 @@ jobs:
|
|
|
43
44
|
name: Create GitHub Release
|
|
44
45
|
needs: [build, publish]
|
|
45
46
|
runs-on: ubuntu-latest
|
|
47
|
+
permissions:
|
|
48
|
+
contents: write # needed to create GitHub Releases
|
|
46
49
|
steps:
|
|
47
50
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
|
48
|
-
- uses: actions/download-artifact@
|
|
51
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8
|
|
49
52
|
with:
|
|
50
53
|
name: dist
|
|
51
54
|
path: dist/
|
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
9
9
|
|
|
10
|
+
## [1.6.0] - 2026-10-15
|
|
11
|
+
|
|
12
|
+
### 🚀 Features
|
|
13
|
+
|
|
14
|
+
- *(faults)* Alert when LR4 laser sensors are dirty
|
|
15
|
+
|
|
16
|
+
### 🔧 Miscellaneous
|
|
17
|
+
|
|
18
|
+
- [**breaking**] Drop Python 3.10 support
|
|
19
|
+
|
|
20
|
+
## [1.5.0] - 2026-10-03
|
|
21
|
+
|
|
22
|
+
### 🐛 Bug Fixes
|
|
23
|
+
|
|
24
|
+
- *(mcp)* Don't overwrite a claude_desktop_config.json that can't be parsed
|
|
25
|
+
- *(mcp)* Cap mcp<2 when /mcp on installs the extra
|
|
26
|
+
|
|
27
|
+
### 👷 CI/CD
|
|
28
|
+
|
|
29
|
+
- *(release)* Scope token permissions to the jobs that need them
|
|
30
|
+
- *(bandit)* Pin bandit to 1.9.4
|
|
31
|
+
|
|
10
32
|
## [1.4.1] - 2026-10-01
|
|
11
33
|
|
|
12
34
|
### 🐛 Bug Fixes
|
|
@@ -14,10 +36,6 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
14
36
|
- *(export)* Reach the full 30 days on the LR4
|
|
15
37
|
- *(tui)* Hang wrapped log lines under their message
|
|
16
38
|
|
|
17
|
-
### 🛡️ Security
|
|
18
|
-
|
|
19
|
-
- *(deps)* Refresh `uv.lock` to patch pyjwt and urllib3 CVEs (#101)
|
|
20
|
-
|
|
21
39
|
### ♻️ Refactor
|
|
22
40
|
|
|
23
41
|
- Group the asher package into core, robot, tui, desktop and mcp
|
|
@@ -32,6 +50,8 @@ Generated by [git-cliff](https://git-cliff.org) from conventional commits.
|
|
|
32
50
|
|
|
33
51
|
### 🔧 Miscellaneous
|
|
34
52
|
|
|
53
|
+
- *(release)* Cut v1.4.1
|
|
54
|
+
- *(release)* Cut v1.4.0
|
|
35
55
|
- Remove claude.yml
|
|
36
56
|
- Remove claude-code-review.yml
|
|
37
57
|
|
|
@@ -277,3 +277,7 @@ Regenerate with `changelog-release X.Y.Z`, never plain `changelog` — the workf
|
|
|
277
277
|
- Mock external deps with `unittest.mock.AsyncMock` for async robot/account methods
|
|
278
278
|
- `from pylitterbot import Account` is a local import inside `_connect_worker` — patch it at `pylitterbot.Account`, not `asher.tui.connection.Account`
|
|
279
279
|
- Coverage: ~76% overall; main gaps are async exception paths and `_connect_worker` auth flow
|
|
280
|
+
|
|
281
|
+
## Temp files and large downloads
|
|
282
|
+
|
|
283
|
+
`/tmp` is tmpfs on this machine, and so is Claude Code's scratchpad (it lives under `/tmp`), so anything written there is held in RAM. Put venvs, model and runtime downloads, extracted archives and build output on disk under `~/.cache/claude-scratch/<task>/` instead, and keep only small throwaway files in the scratchpad. An in-repo `.venv` or `target/` is already on disk and fine.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: asher-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API
|
|
5
5
|
Project-URL: Homepage, https://github.com/karanshukla/asher-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/karanshukla/asher-cli
|
|
@@ -33,21 +33,20 @@ Classifier: Development Status :: 4 - Beta
|
|
|
33
33
|
Classifier: Environment :: Console
|
|
34
34
|
Classifier: License :: OSI Approved :: MIT License
|
|
35
35
|
Classifier: Programming Language :: Python :: 3
|
|
36
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
37
36
|
Classifier: Programming Language :: Python :: 3.11
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.12
|
|
39
38
|
Classifier: Topic :: Home Automation
|
|
40
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
41
40
|
Requires-Dist: keyring>=24.0.0
|
|
42
41
|
Requires-Dist: plyer>=2.1.0
|
|
43
|
-
Requires-Dist: pylitterbot==2025.
|
|
42
|
+
Requires-Dist: pylitterbot==2025.7.0
|
|
44
43
|
Requires-Dist: python-dotenv>=1.0.0
|
|
45
44
|
Requires-Dist: rich>=13.0.0
|
|
46
45
|
Requires-Dist: textual>=0.47.0
|
|
47
46
|
Requires-Dist: tzlocal>=5.4
|
|
48
47
|
Provides-Extra: mcp
|
|
49
48
|
Requires-Dist: mcp<2; extra == 'mcp'
|
|
50
|
-
Requires-Dist: pylitterbot[mcp]==2025.
|
|
49
|
+
Requires-Dist: pylitterbot[mcp]==2025.7.0; extra == 'mcp'
|
|
51
50
|
Provides-Extra: tray
|
|
52
51
|
Requires-Dist: pillow>=10.0; extra == 'tray'
|
|
53
52
|
Requires-Dist: pygobject>=3.42; (sys_platform == 'linux') and extra == 'tray'
|
|
@@ -75,7 +74,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
75
74
|
- Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
|
|
76
75
|
- Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
|
|
77
76
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
78
|
-
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5
|
|
77
|
+
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults, dirty laser sensors (LR4/LR5), LR5 bonnet/drawer; press `d` to dismiss
|
|
79
78
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
80
79
|
- Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
81
80
|
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
|
|
@@ -470,7 +469,7 @@ Scan settings live in `[tool.bandit]` in `pyproject.toml`, so local runs and the
|
|
|
470
469
|
Bandit workflow report identical findings. Results from `main` and pull requests
|
|
471
470
|
are published to the repository's **Security → Code scanning** tab.
|
|
472
471
|
|
|
473
|
-
CI runs on Python 3.
|
|
472
|
+
CI runs on Python 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
|
|
474
473
|
|
|
475
474
|
## Changelog
|
|
476
475
|
|
|
@@ -19,7 +19,7 @@ A Claude Code-style terminal dashboard for monitoring and controlling Litter Rob
|
|
|
19
19
|
- Live status bar — unit name, online/offline, drawer fill level, last activity, cat weight
|
|
20
20
|
- Human-readable robot status — translates raw API states into plain English (`Ready`, `Cleaning`, `Cat Detected`, `Drawer Full`, etc.)
|
|
21
21
|
- Real-time cycling indicator with elapsed time (`⟳ Cycling M:SS`)
|
|
22
|
-
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults (LR5
|
|
22
|
+
- Fault & safety monitoring — model-scoped in-panel alerts for cat detected, pinch, motor/position/gas faults, dirty laser sensors (LR4/LR5), LR5 bonnet/drawer; press `d` to dismiss
|
|
23
23
|
- Cat panel with mode label + status badges (status chip, lock, night light, sleep, wait time) under the art
|
|
24
24
|
- Scrollable activity-history pager — `history [count|all] [--type <kind>]` opens a full-screen, paginated view (arrow keys, `Page Up`/`Page Down`, `Home`/`End`); `c` copies the whole history to the clipboard; `q`/`Esc`/`Enter` to close
|
|
25
25
|
- Commands: `clean`, `status`, `info`, `lock`, `unlock`, `sleep`, `wake`, `night-light on|off|auto|color <hex>`, `night-light-brightness`, `panel-brightness <low|medium|high>` (LR4/LR5), `wait-time`, `power on|off`, `rename`, `insight`, `sleep-schedule [set|disable]`, plus LR5 extras (`privacy`, `volume`, `camera-audio`, `drawer-reset`), `history [count|all] [--type <kind>]`, `export [days|month]`, `help`, `quit`
|
|
@@ -414,7 +414,7 @@ Scan settings live in `[tool.bandit]` in `pyproject.toml`, so local runs and the
|
|
|
414
414
|
Bandit workflow report identical findings. Results from `main` and pull requests
|
|
415
415
|
are published to the repository's **Security → Code scanning** tab.
|
|
416
416
|
|
|
417
|
-
CI runs on Python 3.
|
|
417
|
+
CI runs on Python 3.11 / 3.12 across Ubuntu, Windows, and macOS on every push.
|
|
418
418
|
|
|
419
419
|
## Changelog
|
|
420
420
|
|
|
@@ -10,7 +10,7 @@ identical regardless of how it was invoked.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import csv
|
|
13
|
-
from datetime import datetime, timedelta,
|
|
13
|
+
from datetime import UTC, datetime, timedelta, tzinfo
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING, Any
|
|
16
16
|
|
|
@@ -69,7 +69,7 @@ def _filter_recent(acts: list[Any], cutoff: datetime) -> list[tuple[Any, datetim
|
|
|
69
69
|
if ts_dt is None:
|
|
70
70
|
continue
|
|
71
71
|
if ts_dt.tzinfo is None:
|
|
72
|
-
ts_dt = ts_dt.replace(tzinfo=
|
|
72
|
+
ts_dt = ts_dt.replace(tzinfo=UTC)
|
|
73
73
|
if ts_dt >= cutoff:
|
|
74
74
|
filtered.append((act, ts_dt))
|
|
75
75
|
filtered.sort(key=lambda x: x[1])
|
|
@@ -106,7 +106,7 @@ async def build_history_csv(robot: RobotProtocol, pets: list[Any], days: int, de
|
|
|
106
106
|
``ACTION_LABELS`` / ``activity_raw_text`` so events render identically to
|
|
107
107
|
the ``history`` command output.
|
|
108
108
|
"""
|
|
109
|
-
cutoff = datetime.now(tz=
|
|
109
|
+
cutoff = datetime.now(tz=UTC) - timedelta(days=days)
|
|
110
110
|
# Only the LR4's get_activity_history() takes a start time.
|
|
111
111
|
fetch: Any = robot.get_activity_history
|
|
112
112
|
try:
|
|
@@ -127,7 +127,7 @@ async def build_history_csv(robot: RobotProtocol, pets: list[Any], days: int, de
|
|
|
127
127
|
|
|
128
128
|
local_tz: tzinfo = get_localzone()
|
|
129
129
|
except Exception: # noqa: BLE001 — tz discovery is best-effort; fall back to UTC
|
|
130
|
-
local_tz =
|
|
130
|
+
local_tz = UTC
|
|
131
131
|
|
|
132
132
|
try:
|
|
133
133
|
with dest.open("w", newline="", encoding="utf-8") as f:
|
|
@@ -104,6 +104,9 @@ _MODEL_ENUM_FAULTS: dict[str, tuple[_EnumCheck, ...]] = {
|
|
|
104
104
|
|
|
105
105
|
_MODEL_BOOL_FAULTS: dict[str, tuple[_BoolCheck, ...]] = {
|
|
106
106
|
# LR3 has no component-fault attributes exposed by pylitterbot.
|
|
107
|
+
"LitterRobot4": (
|
|
108
|
+
_BoolCheck("is_laser_dirty", "LASER SENSOR DIRTY — clean globe", SEVERITY_WARN),
|
|
109
|
+
),
|
|
107
110
|
"LitterRobot5": (
|
|
108
111
|
_BoolCheck("is_bonnet_removed", "BONNET OPEN", SEVERITY_WARN),
|
|
109
112
|
_BoolCheck("is_laser_dirty", "LASER SENSOR DIRTY — clean globe", SEVERITY_WARN),
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import re
|
|
7
|
-
from datetime import datetime, time
|
|
7
|
+
from datetime import UTC, datetime, time
|
|
8
8
|
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
@@ -42,8 +42,8 @@ def fmt_ago(dt: datetime | None) -> str:
|
|
|
42
42
|
if dt is None:
|
|
43
43
|
return "never"
|
|
44
44
|
if dt.tzinfo is None:
|
|
45
|
-
dt = dt.replace(tzinfo=
|
|
46
|
-
s = int((datetime.now(
|
|
45
|
+
dt = dt.replace(tzinfo=UTC)
|
|
46
|
+
s = int((datetime.now(UTC) - dt).total_seconds())
|
|
47
47
|
if s < 60:
|
|
48
48
|
return f"{s}s ago"
|
|
49
49
|
if s < 3600:
|
|
@@ -58,8 +58,8 @@ def fmt_until(dt: datetime | None) -> str:
|
|
|
58
58
|
if dt is None:
|
|
59
59
|
return "—"
|
|
60
60
|
if dt.tzinfo is None:
|
|
61
|
-
dt = dt.replace(tzinfo=
|
|
62
|
-
seconds = (dt - datetime.now(
|
|
61
|
+
dt = dt.replace(tzinfo=UTC)
|
|
62
|
+
seconds = (dt - datetime.now(UTC)).total_seconds()
|
|
63
63
|
stamp = dt.date().isoformat()
|
|
64
64
|
# Counted away from now in whole days, so 3 days and an hour ago reads as
|
|
65
65
|
# "3d" in either direction — timedelta.days alone floors that to 4.
|
|
@@ -11,6 +11,10 @@ from pathlib import Path
|
|
|
11
11
|
_SERVER_NAME = "Asher CLI MCP Bridge"
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class McpConfigError(Exception):
|
|
15
|
+
"""A Claude Desktop config file exists but can't be read as a JSON object."""
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
def mcp_extra_installed() -> bool:
|
|
15
19
|
"""Whether pylitterbot's mcp extra (the third-party `mcp` SDK) is importable."""
|
|
16
20
|
return importlib.util.find_spec("mcp") is not None
|
|
@@ -60,10 +64,12 @@ def _load(path: Path) -> dict:
|
|
|
60
64
|
if not path.exists():
|
|
61
65
|
return {}
|
|
62
66
|
try:
|
|
63
|
-
data
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
69
|
+
raise McpConfigError(f"Can't read {path}: {e}") from e
|
|
70
|
+
if not isinstance(data, dict):
|
|
71
|
+
raise McpConfigError(f"Can't read {path}: not a JSON object")
|
|
72
|
+
return data
|
|
67
73
|
|
|
68
74
|
|
|
69
75
|
def _save(path: Path, data: dict) -> None:
|
|
@@ -74,12 +80,13 @@ def _save(path: Path, data: dict) -> None:
|
|
|
74
80
|
def set_mcp_enabled(enabled: bool) -> list[Path]:
|
|
75
81
|
"""Add or remove the MCP server entry in every known config location.
|
|
76
82
|
|
|
77
|
-
Returns the list of config paths actually written.
|
|
83
|
+
Returns the list of config paths actually written. Raises McpConfigError,
|
|
84
|
+
before writing anything, if any existing config can't be read.
|
|
78
85
|
"""
|
|
79
86
|
touched: list[Path] = []
|
|
87
|
+
configs = [(path, _load(path)) for path in config_paths()]
|
|
80
88
|
|
|
81
|
-
for path in
|
|
82
|
-
data = _load(path)
|
|
89
|
+
for path, data in configs:
|
|
83
90
|
servers = data.setdefault("mcpServers", {})
|
|
84
91
|
|
|
85
92
|
if enabled:
|
|
@@ -101,7 +108,9 @@ def mcp_status() -> list[tuple[Path, bool]]:
|
|
|
101
108
|
"""Return (path, enabled) for every known Claude Desktop config location."""
|
|
102
109
|
result = []
|
|
103
110
|
for path in config_paths():
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
try:
|
|
112
|
+
enabled = _SERVER_NAME in _load(path).get("mcpServers", {})
|
|
113
|
+
except McpConfigError:
|
|
114
|
+
enabled = False
|
|
106
115
|
result.append((path, enabled))
|
|
107
116
|
return result
|
|
@@ -439,14 +439,15 @@ async def _ensure_mcp_extra(app: AsherApp) -> bool:
|
|
|
439
439
|
if mcp_extra_installed():
|
|
440
440
|
return True
|
|
441
441
|
|
|
442
|
-
|
|
443
|
-
|
|
442
|
+
# Same requirements as the `mcp` extra in pyproject.toml, cap included.
|
|
443
|
+
requirements = [f"pylitterbot[mcp]=={pkg_version('pylitterbot')}", "mcp<2"]
|
|
444
|
+
app._log_info(f"Installing {' '.join(requirements)}…")
|
|
444
445
|
proc = await asyncio.create_subprocess_exec(
|
|
445
446
|
sys.executable,
|
|
446
447
|
"-m",
|
|
447
448
|
"pip",
|
|
448
449
|
"install",
|
|
449
|
-
|
|
450
|
+
*requirements,
|
|
450
451
|
stdout=asyncio.subprocess.PIPE,
|
|
451
452
|
stderr=asyncio.subprocess.STDOUT,
|
|
452
453
|
)
|
|
@@ -458,7 +459,8 @@ async def _ensure_mcp_extra(app: AsherApp) -> bool:
|
|
|
458
459
|
app._log_err("Failed to install pylitterbot[mcp]:")
|
|
459
460
|
for line in output.splitlines()[-10:]:
|
|
460
461
|
app._log_err(f" {line}")
|
|
461
|
-
|
|
462
|
+
quoted = " ".join(f"'{r}'" for r in requirements)
|
|
463
|
+
app._log_info(f"Try manually: {sys.executable} -m pip install {quoted}")
|
|
462
464
|
return False
|
|
463
465
|
|
|
464
466
|
|
|
@@ -467,7 +469,7 @@ class McpCommand(SlashCommand):
|
|
|
467
469
|
description = "on|off|status Litter-Robot MCP server for Claude Desktop"
|
|
468
470
|
|
|
469
471
|
async def run(self, app: AsherApp, args: list[str]) -> None:
|
|
470
|
-
from ...mcp.config import mcp_status, set_mcp_enabled # noqa: PLC0415
|
|
472
|
+
from ...mcp.config import McpConfigError, mcp_status, set_mcp_enabled # noqa: PLC0415
|
|
471
473
|
|
|
472
474
|
sub = args[0].lower() if args else "status"
|
|
473
475
|
if sub not in ("on", "off", "status"):
|
|
@@ -507,9 +509,12 @@ class McpCommand(SlashCommand):
|
|
|
507
509
|
return
|
|
508
510
|
if not await _ensure_mcp_extra(app):
|
|
509
511
|
return
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
touched = set_mcp_enabled(
|
|
512
|
+
|
|
513
|
+
try:
|
|
514
|
+
touched = set_mcp_enabled(sub == "on")
|
|
515
|
+
except McpConfigError as e:
|
|
516
|
+
app._log_err(f"{e}. Nothing was changed.")
|
|
517
|
+
return
|
|
513
518
|
|
|
514
519
|
verb = "enabled" if sub == "on" else "disabled"
|
|
515
520
|
if touched:
|
|
@@ -22,7 +22,7 @@ vertically regardless of an event's age — every event shows its date.
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
from collections import Counter
|
|
25
|
-
from datetime import
|
|
25
|
+
from datetime import UTC, datetime
|
|
26
26
|
from typing import TYPE_CHECKING, Any
|
|
27
27
|
|
|
28
28
|
from rich.text import Text
|
|
@@ -61,7 +61,7 @@ def _localize(ts: object) -> datetime | None:
|
|
|
61
61
|
if not isinstance(ts, datetime):
|
|
62
62
|
return None
|
|
63
63
|
if ts.tzinfo is None:
|
|
64
|
-
ts = ts.replace(tzinfo=
|
|
64
|
+
ts = ts.replace(tzinfo=UTC)
|
|
65
65
|
return ts.astimezone(get_localzone())
|
|
66
66
|
|
|
67
67
|
|
|
@@ -69,7 +69,7 @@ def _ts_str(local_dt: datetime | None) -> str:
|
|
|
69
69
|
"""Fixed-width timestamp: ``MM/DD HH:MM`` this year, ``YYYY-MM-DD`` older."""
|
|
70
70
|
if local_dt is None:
|
|
71
71
|
return "?"
|
|
72
|
-
if local_dt.year == datetime.now(tz=
|
|
72
|
+
if local_dt.year == datetime.now(tz=UTC).year:
|
|
73
73
|
return local_dt.strftime("%m/%d %H:%M")
|
|
74
74
|
return local_dt.strftime("%Y-%m-%d")
|
|
75
75
|
|
|
@@ -51,7 +51,7 @@ uv run pytest --cov=asher --cov-report=term-missing
|
|
|
51
51
|
Per-file descriptions are in `CLAUDE.md` § Package structure. Pilot gotchas are
|
|
52
52
|
in `CLAUDE.md` § Testing notes.
|
|
53
53
|
|
|
54
|
-
CI matrix: Python 3.
|
|
54
|
+
CI matrix: Python 3.11 / 3.12 × Ubuntu / Windows / macOS.
|
|
55
55
|
|
|
56
56
|
### Coverage
|
|
57
57
|
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "asher-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.6.0"
|
|
4
4
|
description = "Terminal dashboard for Litter Robot (LR3/LR4/LR5) via the Whisker cloud API"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
8
|
keywords = ["litter-robot", "whisker", "tui", "terminal", "smart-home"]
|
|
9
9
|
classifiers = [
|
|
10
10
|
"Development Status :: 4 - Beta",
|
|
11
11
|
"Environment :: Console",
|
|
12
12
|
"License :: OSI Approved :: MIT License",
|
|
13
13
|
"Programming Language :: Python :: 3",
|
|
14
|
-
"Programming Language :: Python :: 3.10",
|
|
15
14
|
"Programming Language :: Python :: 3.11",
|
|
16
15
|
"Programming Language :: Python :: 3.12",
|
|
17
16
|
"Topic :: Home Automation",
|
|
18
17
|
]
|
|
19
18
|
dependencies = [
|
|
20
|
-
"pylitterbot==2025.
|
|
19
|
+
"pylitterbot==2025.7.0",
|
|
21
20
|
"textual>=0.47.0",
|
|
22
21
|
"rich>=13.0.0",
|
|
23
22
|
"python-dotenv>=1.0.0",
|
|
@@ -29,8 +28,9 @@ dependencies = [
|
|
|
29
28
|
[project.optional-dependencies]
|
|
30
29
|
# pylitterbot asks for mcp[cli]>=1.0.0 with no upper bound, but its server does
|
|
31
30
|
# `from mcp.server.fastmcp import FastMCP`, and mcp 2.0 deleted that module.
|
|
32
|
-
# Cap it here
|
|
33
|
-
|
|
31
|
+
# Cap it here, and in _ensure_mcp_extra (tui/commands/slash.py), until
|
|
32
|
+
# pylitterbot ships a 2.x-compatible release.
|
|
33
|
+
mcp = ["pylitterbot[mcp]==2025.7.0", "mcp<2"]
|
|
34
34
|
# PyGObject is what puts pystray on its AppIndicator backend. Without it pystray
|
|
35
35
|
# silently falls back to a legacy XEmbed icon, which KDE Plasma and GNOME no
|
|
36
36
|
# longer host at all — the watcher runs, draws an icon, and nothing shows it.
|
|
@@ -81,7 +81,7 @@ lint = "ruff check ."
|
|
|
81
81
|
fmt = "ruff format --check ."
|
|
82
82
|
types = "mypy asher/ --ignore-missing-imports"
|
|
83
83
|
test = "pytest tests/ -v --tb=short"
|
|
84
|
-
security.cmd = "uvx --from bandit[toml] bandit -c pyproject.toml -r ."
|
|
84
|
+
security.cmd = "uvx --from bandit[toml]==1.9.4 bandit -c pyproject.toml -r ."
|
|
85
85
|
security.help = "run the same Bandit scan as the Bandit workflow"
|
|
86
86
|
fix.sequence = [{cmd = "ruff check --fix ."}, {cmd = "ruff format ."}]
|
|
87
87
|
dev = "textual run --dev asher/__main__.py"
|
|
@@ -104,7 +104,7 @@ packages = ["asher"]
|
|
|
104
104
|
|
|
105
105
|
[tool.ruff]
|
|
106
106
|
line-length = 100
|
|
107
|
-
target-version = "
|
|
107
|
+
target-version = "py311"
|
|
108
108
|
# Ruff 0.16 added "*.md" to the default include list, which pulls the Python
|
|
109
109
|
# snippets in docs/ and .claude/skills/ into `ruff format`. Those are
|
|
110
110
|
# illustrative, not compiled, so keep the formatter scoped to real source.
|
|
@@ -124,14 +124,14 @@ known-first-party = ["asher"]
|
|
|
124
124
|
# ── Mypy ──────────────────────────────────────────────────────────────────────
|
|
125
125
|
|
|
126
126
|
[tool.mypy]
|
|
127
|
-
python_version = "3.
|
|
127
|
+
python_version = "3.11"
|
|
128
128
|
warn_return_any = true
|
|
129
129
|
ignore_missing_imports = true # pylitterbot and textual have incomplete stubs
|
|
130
130
|
|
|
131
131
|
# ── bump-my-version ───────────────────────────────────────────────────────────
|
|
132
132
|
|
|
133
133
|
[tool.bumpversion]
|
|
134
|
-
current_version = "1.
|
|
134
|
+
current_version = "1.6.0"
|
|
135
135
|
commit = true
|
|
136
136
|
tag = true
|
|
137
137
|
tag_name = "v{new_version}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -25,7 +25,7 @@ def mock_robot():
|
|
|
25
25
|
r.panel_lock_enabled = False
|
|
26
26
|
r.night_light_mode_enabled = False
|
|
27
27
|
r.serial = "ABC123"
|
|
28
|
-
r.last_seen = datetime.now(
|
|
28
|
+
r.last_seen = datetime.now(UTC)
|
|
29
29
|
# Fault sources in their healthy/no-fault state (enums are truthy even
|
|
30
30
|
# when healthy, so they must be set explicitly to avoid false positives).
|
|
31
31
|
r.globe_motor_fault_status = GlobeMotorFaultStatus.NONE
|
|
@@ -7,7 +7,7 @@ about how ``action`` (``str | LitterBoxStatus``) is read.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from datetime import
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
11
|
from types import SimpleNamespace
|
|
12
12
|
|
|
13
13
|
from pylitterbot.enums import LitterBoxStatus
|
|
@@ -29,7 +29,7 @@ def _act(action, *, weight=None, pet_id=None, timestamp=None):
|
|
|
29
29
|
Using ``SimpleNamespace`` lets us attach them without subclassing.
|
|
30
30
|
"""
|
|
31
31
|
return SimpleNamespace(
|
|
32
|
-
timestamp=timestamp or datetime(2026, 6, 14, 12, 0, tzinfo=
|
|
32
|
+
timestamp=timestamp or datetime(2026, 6, 14, 12, 0, tzinfo=UTC),
|
|
33
33
|
action=action,
|
|
34
34
|
weight=weight,
|
|
35
35
|
pet_id=pet_id,
|
|
@@ -52,7 +52,7 @@ class TestActivityRawText:
|
|
|
52
52
|
|
|
53
53
|
def test_missing_action_attribute_falls_back_to_str_none(self):
|
|
54
54
|
# Defensive: a malformed object with no .action attr should not raise.
|
|
55
|
-
assert activity_raw_text(SimpleNamespace(timestamp=datetime.now(
|
|
55
|
+
assert activity_raw_text(SimpleNamespace(timestamp=datetime.now(UTC))) == "None"
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
# ── format_activity ──────────────────────────────────────────────────────────
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -23,7 +23,7 @@ def mock_robot():
|
|
|
23
23
|
robot.panel_lock_enabled = False
|
|
24
24
|
robot.night_light_mode_enabled = False
|
|
25
25
|
robot.serial = "LR12345"
|
|
26
|
-
robot.last_seen = datetime.now(
|
|
26
|
+
robot.last_seen = datetime.now(UTC)
|
|
27
27
|
robot.pet_weight = 10.5
|
|
28
28
|
robot.refresh = AsyncMock()
|
|
29
29
|
robot.start_cleaning = AsyncMock(return_value=True)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from datetime import
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
6
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
7
7
|
|
|
8
8
|
import pytest
|
|
@@ -24,7 +24,7 @@ def connected_app():
|
|
|
24
24
|
robot.panel_lock_enabled = False
|
|
25
25
|
robot.night_light_mode_enabled = False
|
|
26
26
|
robot.serial = "LR12345"
|
|
27
|
-
robot.last_seen = datetime.now(
|
|
27
|
+
robot.last_seen = datetime.now(UTC)
|
|
28
28
|
robot.pet_weight = 10.5
|
|
29
29
|
robot.refresh = AsyncMock()
|
|
30
30
|
robot.start_cleaning = AsyncMock(return_value=True)
|
|
@@ -19,7 +19,7 @@ so the command bar is live but no real cloud calls happen.
|
|
|
19
19
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
|
-
from datetime import
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
23
|
from types import SimpleNamespace
|
|
24
24
|
from unittest.mock import AsyncMock, MagicMock
|
|
25
25
|
|
|
@@ -72,7 +72,7 @@ def connected_app():
|
|
|
72
72
|
robot.panel_lock_enabled = False
|
|
73
73
|
robot.night_light_mode_enabled = False
|
|
74
74
|
robot.serial = "LR12345"
|
|
75
|
-
robot.last_seen = datetime.now(
|
|
75
|
+
robot.last_seen = datetime.now(UTC)
|
|
76
76
|
robot.pet_weight = 10.5
|
|
77
77
|
robot.refresh = AsyncMock()
|
|
78
78
|
robot.start_cleaning = AsyncMock(return_value=True)
|