pushraker 0.1.0.dev0__tar.gz → 0.2.1__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.
- pushraker-0.2.1/PKG-INFO +115 -0
- pushraker-0.2.1/README.md +95 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/pyproject.toml +1 -1
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker/__init__.py +1 -1
- pushraker-0.2.1/src/pushraker/__main__.py +354 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker/component.py +747 -195
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker/discord.py +9 -3
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker/email_html.py +1 -1
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker/images.py +1 -1
- pushraker-0.2.1/src/pushraker/klipper/pushraker.cfg +82 -0
- pushraker-0.2.1/src/pushraker/printers/__init__.py +102 -0
- pushraker-0.2.1/src/pushraker/printers/snapmaker_u1.py +51 -0
- pushraker-0.2.1/src/pushraker/settings.py +1273 -0
- pushraker-0.2.1/src/pushraker/web/index.html +1039 -0
- pushraker-0.2.1/src/pushraker.egg-info/PKG-INFO +115 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker.egg-info/SOURCES.txt +8 -1
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/tests/test_pushraker.py +371 -12
- pushraker-0.2.1/tests/test_settings.py +471 -0
- pushraker-0.1.0.dev0/PKG-INFO +0 -49
- pushraker-0.1.0.dev0/README.md +0 -29
- pushraker-0.1.0.dev0/src/pushraker.egg-info/PKG-INFO +0 -49
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/LICENSE +0 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/setup.cfg +0 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker.egg-info/dependency_links.txt +0 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker.egg-info/requires.txt +0 -0
- {pushraker-0.1.0.dev0 → pushraker-0.2.1}/src/pushraker.egg-info/top_level.txt +0 -0
pushraker-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pushraker
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Push notifications for Klipper printers: a Moonraker component with pictures, sent through Apprise (ntfy, Pushover, Telegram, Discord, webhooks, email)
|
|
5
|
+
Author: noon92
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/noon92/pushraker
|
|
8
|
+
Project-URL: Issues, https://github.com/noon92/pushraker/issues
|
|
9
|
+
Keywords: klipper,moonraker,notifications,ntfy,apprise,3d-printing
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Topic :: System :: Monitoring
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: apprise>=1.9
|
|
17
|
+
Requires-Dist: requests
|
|
18
|
+
Requires-Dist: Pillow>=10
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# pushraker
|
|
22
|
+
|
|
23
|
+
Push notifications for Klipper printers, as a Moonraker component: print
|
|
24
|
+
started, paused, resumed, complete, cancelled, progress, first layer, faults
|
|
25
|
+
and shutdowns, power-loss recovery, chamber temperature, filament (Spoolman,
|
|
26
|
+
backup spools, MMUs), with pictures (camera snapshot, model preview, spool
|
|
27
|
+
drawings). Sent through [Apprise](https://github.com/caronc/apprise): ntfy,
|
|
28
|
+
Pushover, Gotify, Telegram, Discord, a webhook, email. Configured from its
|
|
29
|
+
own web page, live, with no Moonraker or Klipper restart.
|
|
30
|
+
|
|
31
|
+
It grew out of the Push Notifications feature of the
|
|
32
|
+
[Snapmaker U1 Extended Firmware](https://github.com/paxx12-snapmaker-u1/SnapmakerU1-Extended-Firmware),
|
|
33
|
+
where it is built in; the user documentation lives there
|
|
34
|
+
for now: [notifications](https://snapmakeru1-extended-firmware.pages.dev/notifications).
|
|
35
|
+
|
|
36
|
+
**Status: 0.2.** The component has been in
|
|
37
|
+
use on the Snapmaker U1 for a while; the page, the settings file and the
|
|
38
|
+
installer are new. Other printers: the generic parts work (Klipper's
|
|
39
|
+
standard objects: print_stats, exclude_object, heater_generic, tmc
|
|
40
|
+
drivers, Moonraker's webcams and Spoolman); printer-specific parts are
|
|
41
|
+
behind `printers/`, one module per printer; `snapmaker_u1.py` is the
|
|
42
|
+
first. Notifications a printer can't produce (the U1's touchscreen
|
|
43
|
+
calibrations, coded warnings, power-loss recovery, toolhead-map refills,
|
|
44
|
+
MMU slots) start off on other printers and are left off the page; turn
|
|
45
|
+
one on in `pushraker.conf` and it shows on the page and is sent like any
|
|
46
|
+
other.
|
|
47
|
+
|
|
48
|
+
## Install (any Moonraker printer)
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install pushraker # into the Python Moonraker runs with
|
|
52
|
+
python -m pushraker install # ~/moonraker and ~/printer_data by default
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`install` puts a one-file shim into Moonraker's `components/` folder, adds
|
|
56
|
+
`[pushraker]` to `moonraker.conf`, includes the Klipper macro
|
|
57
|
+
file from `printer.cfg`, and creates `printer_data/config/pushraker.conf`.
|
|
58
|
+
Restart Moonraker and Klipper. Then serve the settings page
|
|
59
|
+
(`src/pushraker/web/index.html`, or `python -c "import pushraker, os;
|
|
60
|
+
print(os.path.dirname(pushraker.__file__) + '/web')"`) from the same nginx
|
|
61
|
+
that serves Fluidd or Mainsail, e.g.
|
|
62
|
+
|
|
63
|
+
```nginx
|
|
64
|
+
location /notifications/ {
|
|
65
|
+
alias /path/to/site-packages/pushraker/web/;
|
|
66
|
+
index index.html;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
and open `http://<printer>/notifications/`. The page talks to Moonraker
|
|
71
|
+
through the `/server/` proxy of that nginx; without one it falls back to
|
|
72
|
+
Moonraker's own port, which needs the page's origin in `cors_domains`
|
|
73
|
+
under `[authorization]` in `moonraker.conf`. With Moonraker's
|
|
74
|
+
authorization on, the page asks for an API key the first time.
|
|
75
|
+
|
|
76
|
+
Call `_PRINT_END_NTFY` at the start of your `PRINT_END` macro and
|
|
77
|
+
`_CANCEL_PRINT_NTFY` at the start of `CANCEL_PRINT` for the print-end and
|
|
78
|
+
cancel snapshots to be taken before the printer parks (the Snapmaker U1
|
|
79
|
+
extended firmware does that through its hook system).
|
|
80
|
+
|
|
81
|
+
`python -m pushraker install --help` lists the paths and options (include
|
|
82
|
+
directories, the printer's default name, the error-code index link, moving
|
|
83
|
+
settings over from an INI section). `python -m pushraker uninstall` takes
|
|
84
|
+
it out again; `--purge` also removes the settings and secrets.
|
|
85
|
+
|
|
86
|
+
## Where things live on the printer
|
|
87
|
+
|
|
88
|
+
- `printer_data/config/pushraker.conf`: the settings, a plain INI file
|
|
89
|
+
(also editable by hand; the component re-reads it when it changes).
|
|
90
|
+
- `printer_data/pushraker.secrets`: tokens and passwords, mode 0600,
|
|
91
|
+
outside the config folder so Moonraker never serves it.
|
|
92
|
+
- Moonraker endpoints: `GET/POST /server/pushraker/config`,
|
|
93
|
+
`POST /server/pushraker/test`, `GET /server/pushraker/status` (also
|
|
94
|
+
under `/server/push_notifications/`, the 0.1 names).
|
|
95
|
+
|
|
96
|
+
## Layout
|
|
97
|
+
|
|
98
|
+
- `src/pushraker/component.py` — the Moonraker component
|
|
99
|
+
- `src/pushraker/settings.py` — the settings: validation, storage, the
|
|
100
|
+
Apprise URLs built from them
|
|
101
|
+
- `src/pushraker/printers/` — what differs between printers (the harness
|
|
102
|
+
side of the package), one module per printer; `snapmaker_u1.py` is the
|
|
103
|
+
first
|
|
104
|
+
- `src/pushraker/images.py` — spool pictures (Pillow)
|
|
105
|
+
- `src/pushraker/email_html.py`, `src/pushraker/discord.py` — email card,
|
|
106
|
+
Discord senders
|
|
107
|
+
- `src/pushraker/web/index.html` — the settings page
|
|
108
|
+
- `src/pushraker/klipper/pushraker.cfg` — the Klipper macro file
|
|
109
|
+
- `src/pushraker/__main__.py` — `python -m pushraker install|uninstall|status`
|
|
110
|
+
- `tests/` — `python tests/test_pushraker.py` (the component against a
|
|
111
|
+
stand-in Moonraker; also runs `test_settings.py`)
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
GPL-3.0-or-later.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# pushraker
|
|
2
|
+
|
|
3
|
+
Push notifications for Klipper printers, as a Moonraker component: print
|
|
4
|
+
started, paused, resumed, complete, cancelled, progress, first layer, faults
|
|
5
|
+
and shutdowns, power-loss recovery, chamber temperature, filament (Spoolman,
|
|
6
|
+
backup spools, MMUs), with pictures (camera snapshot, model preview, spool
|
|
7
|
+
drawings). Sent through [Apprise](https://github.com/caronc/apprise): ntfy,
|
|
8
|
+
Pushover, Gotify, Telegram, Discord, a webhook, email. Configured from its
|
|
9
|
+
own web page, live, with no Moonraker or Klipper restart.
|
|
10
|
+
|
|
11
|
+
It grew out of the Push Notifications feature of the
|
|
12
|
+
[Snapmaker U1 Extended Firmware](https://github.com/paxx12-snapmaker-u1/SnapmakerU1-Extended-Firmware),
|
|
13
|
+
where it is built in; the user documentation lives there
|
|
14
|
+
for now: [notifications](https://snapmakeru1-extended-firmware.pages.dev/notifications).
|
|
15
|
+
|
|
16
|
+
**Status: 0.2.** The component has been in
|
|
17
|
+
use on the Snapmaker U1 for a while; the page, the settings file and the
|
|
18
|
+
installer are new. Other printers: the generic parts work (Klipper's
|
|
19
|
+
standard objects: print_stats, exclude_object, heater_generic, tmc
|
|
20
|
+
drivers, Moonraker's webcams and Spoolman); printer-specific parts are
|
|
21
|
+
behind `printers/`, one module per printer; `snapmaker_u1.py` is the
|
|
22
|
+
first. Notifications a printer can't produce (the U1's touchscreen
|
|
23
|
+
calibrations, coded warnings, power-loss recovery, toolhead-map refills,
|
|
24
|
+
MMU slots) start off on other printers and are left off the page; turn
|
|
25
|
+
one on in `pushraker.conf` and it shows on the page and is sent like any
|
|
26
|
+
other.
|
|
27
|
+
|
|
28
|
+
## Install (any Moonraker printer)
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install pushraker # into the Python Moonraker runs with
|
|
32
|
+
python -m pushraker install # ~/moonraker and ~/printer_data by default
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`install` puts a one-file shim into Moonraker's `components/` folder, adds
|
|
36
|
+
`[pushraker]` to `moonraker.conf`, includes the Klipper macro
|
|
37
|
+
file from `printer.cfg`, and creates `printer_data/config/pushraker.conf`.
|
|
38
|
+
Restart Moonraker and Klipper. Then serve the settings page
|
|
39
|
+
(`src/pushraker/web/index.html`, or `python -c "import pushraker, os;
|
|
40
|
+
print(os.path.dirname(pushraker.__file__) + '/web')"`) from the same nginx
|
|
41
|
+
that serves Fluidd or Mainsail, e.g.
|
|
42
|
+
|
|
43
|
+
```nginx
|
|
44
|
+
location /notifications/ {
|
|
45
|
+
alias /path/to/site-packages/pushraker/web/;
|
|
46
|
+
index index.html;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
and open `http://<printer>/notifications/`. The page talks to Moonraker
|
|
51
|
+
through the `/server/` proxy of that nginx; without one it falls back to
|
|
52
|
+
Moonraker's own port, which needs the page's origin in `cors_domains`
|
|
53
|
+
under `[authorization]` in `moonraker.conf`. With Moonraker's
|
|
54
|
+
authorization on, the page asks for an API key the first time.
|
|
55
|
+
|
|
56
|
+
Call `_PRINT_END_NTFY` at the start of your `PRINT_END` macro and
|
|
57
|
+
`_CANCEL_PRINT_NTFY` at the start of `CANCEL_PRINT` for the print-end and
|
|
58
|
+
cancel snapshots to be taken before the printer parks (the Snapmaker U1
|
|
59
|
+
extended firmware does that through its hook system).
|
|
60
|
+
|
|
61
|
+
`python -m pushraker install --help` lists the paths and options (include
|
|
62
|
+
directories, the printer's default name, the error-code index link, moving
|
|
63
|
+
settings over from an INI section). `python -m pushraker uninstall` takes
|
|
64
|
+
it out again; `--purge` also removes the settings and secrets.
|
|
65
|
+
|
|
66
|
+
## Where things live on the printer
|
|
67
|
+
|
|
68
|
+
- `printer_data/config/pushraker.conf`: the settings, a plain INI file
|
|
69
|
+
(also editable by hand; the component re-reads it when it changes).
|
|
70
|
+
- `printer_data/pushraker.secrets`: tokens and passwords, mode 0600,
|
|
71
|
+
outside the config folder so Moonraker never serves it.
|
|
72
|
+
- Moonraker endpoints: `GET/POST /server/pushraker/config`,
|
|
73
|
+
`POST /server/pushraker/test`, `GET /server/pushraker/status` (also
|
|
74
|
+
under `/server/push_notifications/`, the 0.1 names).
|
|
75
|
+
|
|
76
|
+
## Layout
|
|
77
|
+
|
|
78
|
+
- `src/pushraker/component.py` — the Moonraker component
|
|
79
|
+
- `src/pushraker/settings.py` — the settings: validation, storage, the
|
|
80
|
+
Apprise URLs built from them
|
|
81
|
+
- `src/pushraker/printers/` — what differs between printers (the harness
|
|
82
|
+
side of the package), one module per printer; `snapmaker_u1.py` is the
|
|
83
|
+
first
|
|
84
|
+
- `src/pushraker/images.py` — spool pictures (Pillow)
|
|
85
|
+
- `src/pushraker/email_html.py`, `src/pushraker/discord.py` — email card,
|
|
86
|
+
Discord senders
|
|
87
|
+
- `src/pushraker/web/index.html` — the settings page
|
|
88
|
+
- `src/pushraker/klipper/pushraker.cfg` — the Klipper macro file
|
|
89
|
+
- `src/pushraker/__main__.py` — `python -m pushraker install|uninstall|status`
|
|
90
|
+
- `tests/` — `python tests/test_pushraker.py` (the component against a
|
|
91
|
+
stand-in Moonraker; also runs `test_settings.py`)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
GPL-3.0-or-later.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pushraker"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Push notifications for Klipper printers: a Moonraker component with pictures, sent through Apprise (ntfy, Pushover, Telegram, Discord, webhooks, email)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
# SPDX-PackageHomePage: https://github.com/noon92/pushraker
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026 @noon92
|
|
4
|
+
|
|
5
|
+
"""python -m pushraker install|uninstall|status: wire the package into a
|
|
6
|
+
Moonraker + Klipper installation.
|
|
7
|
+
|
|
8
|
+
install put the component shim into Moonraker's components folder,
|
|
9
|
+
enable [pushraker] in Moonraker's config, include
|
|
10
|
+
the Klipper macro file, and (once) move settings over from
|
|
11
|
+
an INI section such as the Snapmaker U1 extended firmware's
|
|
12
|
+
extended2.cfg [notifications]
|
|
13
|
+
uninstall take those out again (the settings and secrets stay unless
|
|
14
|
+
--purge)
|
|
15
|
+
status say what is in place
|
|
16
|
+
|
|
17
|
+
Paths default to a standard printer_data layout under the running user's
|
|
18
|
+
home (~/moonraker, ~/printer_data); the Snapmaker U1 extended firmware's
|
|
19
|
+
harness passes its own.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
import argparse
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import sys
|
|
27
|
+
from typing import List, Optional
|
|
28
|
+
|
|
29
|
+
from . import __version__
|
|
30
|
+
from . import settings as cfgmod
|
|
31
|
+
|
|
32
|
+
SHIM_NAME = "pushraker.py"
|
|
33
|
+
SECTION = "pushraker"
|
|
34
|
+
# What 0.1.x installed under the old component name: replaced on install
|
|
35
|
+
OLD_SHIM_NAME = "push_notifications.py"
|
|
36
|
+
OLD_SECTION = "push_notifications"
|
|
37
|
+
MOONRAKER_INCLUDE = "pushraker.cfg"
|
|
38
|
+
KLIPPER_INCLUDE = "pushraker.cfg"
|
|
39
|
+
SHIM = '''# pushraker: Moonraker loads components from this folder; this file hands
|
|
40
|
+
# over to the installed package. Written by `python -m pushraker install`.
|
|
41
|
+
import sys
|
|
42
|
+
for _p in {paths!r}:
|
|
43
|
+
if _p and _p not in sys.path:
|
|
44
|
+
sys.path.append(_p)
|
|
45
|
+
from pushraker.component import load_component # noqa: E402,F401
|
|
46
|
+
'''
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def home(sub: str) -> str:
|
|
50
|
+
return os.path.join(os.path.expanduser("~"), sub)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse(argv: List[str]) -> argparse.Namespace:
|
|
54
|
+
p = argparse.ArgumentParser(prog="python -m pushraker",
|
|
55
|
+
description=__doc__.split("\n\n")[0])
|
|
56
|
+
p.add_argument("command", choices=["install", "uninstall", "status"])
|
|
57
|
+
p.add_argument("--moonraker-dir", default=home("moonraker"),
|
|
58
|
+
help="Moonraker's source directory (holds moonraker/components)")
|
|
59
|
+
p.add_argument("--data-dir", default=home("printer_data"),
|
|
60
|
+
help="printer_data (config/, the secrets file lives here)")
|
|
61
|
+
p.add_argument("--moonraker-conf", default=None,
|
|
62
|
+
help="moonraker.conf (default: <data-dir>/config/moonraker.conf)")
|
|
63
|
+
p.add_argument("--moonraker-include-dir", default=None,
|
|
64
|
+
help="a directory moonraker.conf already includes: the "
|
|
65
|
+
"[pushraker] section is written there as "
|
|
66
|
+
"pushraker.cfg instead of into moonraker.conf")
|
|
67
|
+
p.add_argument("--printer-cfg", default=None,
|
|
68
|
+
help="printer.cfg (default: <data-dir>/config/printer.cfg)")
|
|
69
|
+
p.add_argument("--klipper-include-dir", default=None,
|
|
70
|
+
help="a directory printer.cfg already includes: the macro "
|
|
71
|
+
"file is copied there instead of being included from "
|
|
72
|
+
"printer.cfg")
|
|
73
|
+
p.add_argument("--site-packages", action="append", default=[],
|
|
74
|
+
help="extra sys.path entries for the shim (the package's "
|
|
75
|
+
"venv, when Moonraker's Python can't see it)")
|
|
76
|
+
p.add_argument("--name", default="Printer",
|
|
77
|
+
help="the printer's default name in notifications")
|
|
78
|
+
p.add_argument("--error-index-url", default="",
|
|
79
|
+
help="the printer maker's error-code index, linked from "
|
|
80
|
+
"fault notifications")
|
|
81
|
+
p.add_argument("--import-ini", nargs=2, metavar=("FILE", "SECTION"),
|
|
82
|
+
help="settings to move into pushraker.conf when it doesn't "
|
|
83
|
+
"exist yet (e.g. extended2.cfg notifications)")
|
|
84
|
+
p.add_argument("--import-secrets", nargs=2, metavar=("FILE", "SECTION"),
|
|
85
|
+
help="the secrets that go with --import-ini (e.g. "
|
|
86
|
+
"moonraker.secrets notifications)")
|
|
87
|
+
p.add_argument("--purge", action="store_true",
|
|
88
|
+
help="uninstall: also remove pushraker.conf and the secrets")
|
|
89
|
+
p.add_argument("--owner", default="",
|
|
90
|
+
help="user[:group] the written files belong to (default: "
|
|
91
|
+
"whoever owns the directory they go into)")
|
|
92
|
+
return p.parse_args(argv)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write(path: str, text: str, mode: int = 0o644,
|
|
96
|
+
owner: str = "") -> bool:
|
|
97
|
+
"""Write if different; returns whether it changed. Files take the
|
|
98
|
+
owner of their directory unless --owner says otherwise."""
|
|
99
|
+
try:
|
|
100
|
+
with open(path, encoding="utf-8") as f:
|
|
101
|
+
if f.read() == text:
|
|
102
|
+
return False
|
|
103
|
+
except (OSError, UnicodeDecodeError):
|
|
104
|
+
pass
|
|
105
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
106
|
+
tmp = path + ".tmp"
|
|
107
|
+
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
|
108
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
109
|
+
f.write(text)
|
|
110
|
+
os.chmod(tmp, mode)
|
|
111
|
+
chown(tmp, owner, os.path.dirname(path))
|
|
112
|
+
os.replace(tmp, path)
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def chown(path: str, owner: str, like: str) -> None:
|
|
117
|
+
try:
|
|
118
|
+
if owner:
|
|
119
|
+
import pwd
|
|
120
|
+
import grp
|
|
121
|
+
user, _, group = owner.partition(":")
|
|
122
|
+
uid = pwd.getpwnam(user).pw_uid
|
|
123
|
+
gid = grp.getgrnam(group).gr_gid if group else \
|
|
124
|
+
pwd.getpwnam(user).pw_gid
|
|
125
|
+
else:
|
|
126
|
+
st = os.stat(like)
|
|
127
|
+
uid, gid = st.st_uid, st.st_gid
|
|
128
|
+
os.chown(path, uid, gid)
|
|
129
|
+
except (KeyError, OSError, ImportError):
|
|
130
|
+
pass
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def remove(path: str) -> bool:
|
|
134
|
+
try:
|
|
135
|
+
os.remove(path)
|
|
136
|
+
return True
|
|
137
|
+
except OSError:
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
SAVE_CONFIG_HEADER = "#*# <---------------------- SAVE_CONFIG"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def read_lines(conf: str) -> List[str]:
|
|
145
|
+
try:
|
|
146
|
+
with open(conf, encoding="utf-8") as f:
|
|
147
|
+
return f.read().splitlines(True)
|
|
148
|
+
except (OSError, UnicodeDecodeError):
|
|
149
|
+
return []
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def is_header(line: str, section: str) -> bool:
|
|
153
|
+
s = line.strip()
|
|
154
|
+
return s.startswith("[") and s.endswith("]") and \
|
|
155
|
+
s.lower().replace(" ", "") == f"[{section}]".lower().replace(" ", "")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def ensure_section(conf: str, section: str, text: str, owner: str) -> bool:
|
|
159
|
+
"""Add a section to a config file if it has none: before Klipper's
|
|
160
|
+
SAVE_CONFIG block when there is one (Klipper refuses anything after
|
|
161
|
+
that header and drops the saved calibration), else at the end."""
|
|
162
|
+
lines = read_lines(conf)
|
|
163
|
+
if any(is_header(ln, section) for ln in lines):
|
|
164
|
+
return False
|
|
165
|
+
at = next((i for i, ln in enumerate(lines)
|
|
166
|
+
if ln.startswith(SAVE_CONFIG_HEADER)), len(lines))
|
|
167
|
+
block = text if text.endswith("\n") else text + "\n"
|
|
168
|
+
before = "".join(lines[:at])
|
|
169
|
+
if before and not before.endswith("\n"):
|
|
170
|
+
before += "\n"
|
|
171
|
+
if before and not before.endswith("\n\n"):
|
|
172
|
+
before += "\n"
|
|
173
|
+
after = "".join(lines[at:])
|
|
174
|
+
return write(conf, before + block + ("\n" + after if after else ""),
|
|
175
|
+
owner=owner)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def drop_section(conf: str, section: str, owner: str) -> bool:
|
|
179
|
+
"""Remove a section from a config file: an [include ...] is its one
|
|
180
|
+
line; anything else runs to the next header or Klipper's SAVE_CONFIG
|
|
181
|
+
block."""
|
|
182
|
+
lines = read_lines(conf)
|
|
183
|
+
if not lines:
|
|
184
|
+
return False
|
|
185
|
+
out: List[str] = []
|
|
186
|
+
skipping = False
|
|
187
|
+
one_line = section.lower().startswith("include ")
|
|
188
|
+
for ln in lines:
|
|
189
|
+
s = ln.strip()
|
|
190
|
+
if is_header(ln, section):
|
|
191
|
+
if one_line:
|
|
192
|
+
continue
|
|
193
|
+
skipping = True
|
|
194
|
+
continue
|
|
195
|
+
if skipping and ((s.startswith("[") and s.endswith("]"))
|
|
196
|
+
or ln.startswith(SAVE_CONFIG_HEADER)):
|
|
197
|
+
skipping = False
|
|
198
|
+
if not skipping:
|
|
199
|
+
out.append(ln)
|
|
200
|
+
text = "".join(out)
|
|
201
|
+
if text == "".join(lines):
|
|
202
|
+
return False
|
|
203
|
+
return write(conf, text, owner=owner)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def macro_file() -> str:
|
|
207
|
+
return os.path.join(os.path.dirname(__file__), "klipper", KLIPPER_INCLUDE)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def places(a: argparse.Namespace) -> dict:
|
|
211
|
+
config_dir = os.path.join(a.data_dir, "config")
|
|
212
|
+
return dict(
|
|
213
|
+
shim=os.path.join(a.moonraker_dir, "moonraker", "components",
|
|
214
|
+
SHIM_NAME),
|
|
215
|
+
old_shim=os.path.join(a.moonraker_dir, "moonraker", "components",
|
|
216
|
+
OLD_SHIM_NAME),
|
|
217
|
+
moonraker_conf=a.moonraker_conf
|
|
218
|
+
or os.path.join(config_dir, "moonraker.conf"),
|
|
219
|
+
moonraker_include=os.path.join(a.moonraker_include_dir,
|
|
220
|
+
MOONRAKER_INCLUDE)
|
|
221
|
+
if a.moonraker_include_dir else "",
|
|
222
|
+
printer_cfg=a.printer_cfg or os.path.join(config_dir, "printer.cfg"),
|
|
223
|
+
klipper_include=os.path.join(a.klipper_include_dir, KLIPPER_INCLUDE)
|
|
224
|
+
if a.klipper_include_dir else os.path.join(config_dir,
|
|
225
|
+
KLIPPER_INCLUDE),
|
|
226
|
+
store=cfgmod.Store(config_dir, a.data_dir, name=a.name))
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def section_text(a: argparse.Namespace) -> str:
|
|
230
|
+
lines = [f"[{SECTION}]",
|
|
231
|
+
"# pushraker (python -m pushraker install); settings are in "
|
|
232
|
+
"pushraker.conf",
|
|
233
|
+
f"name: {a.name}"]
|
|
234
|
+
if a.error_index_url:
|
|
235
|
+
lines.append(f"error_index_url: {a.error_index_url}")
|
|
236
|
+
return "\n".join(lines) + "\n"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_install(a: argparse.Namespace) -> int:
|
|
240
|
+
p = places(a)
|
|
241
|
+
changed = []
|
|
242
|
+
if p["moonraker_include"] and os.path.realpath(p["moonraker_include"]) \
|
|
243
|
+
== os.path.realpath(p["klipper_include"]):
|
|
244
|
+
print("ERROR: --moonraker-include-dir and the Klipper include "
|
|
245
|
+
"directory are the same; both files are named pushraker.cfg")
|
|
246
|
+
return 1
|
|
247
|
+
if not os.path.isdir(os.path.dirname(p["shim"])):
|
|
248
|
+
print(f"ERROR: no Moonraker components directory at "
|
|
249
|
+
f"{os.path.dirname(p['shim'])} (--moonraker-dir)")
|
|
250
|
+
return 1
|
|
251
|
+
if write(p["shim"], SHIM.format(paths=list(a.site_packages)),
|
|
252
|
+
owner=a.owner):
|
|
253
|
+
changed.append(p["shim"])
|
|
254
|
+
# 0.1.x's names: the old shim and section would load the component a
|
|
255
|
+
# second time
|
|
256
|
+
if remove(p["old_shim"]):
|
|
257
|
+
changed.append(f"removed {p['old_shim']}")
|
|
258
|
+
if p["moonraker_include"]:
|
|
259
|
+
if write(p["moonraker_include"], section_text(a), owner=a.owner):
|
|
260
|
+
changed.append(p["moonraker_include"])
|
|
261
|
+
else:
|
|
262
|
+
if drop_section(p["moonraker_conf"], OLD_SECTION, a.owner):
|
|
263
|
+
changed.append(f"[{OLD_SECTION}] removed from {p['moonraker_conf']}")
|
|
264
|
+
if ensure_section(p["moonraker_conf"], SECTION, section_text(a),
|
|
265
|
+
a.owner):
|
|
266
|
+
changed.append(p["moonraker_conf"])
|
|
267
|
+
with open(macro_file()) as f:
|
|
268
|
+
macros = f.read()
|
|
269
|
+
if write(p["klipper_include"], macros, owner=a.owner):
|
|
270
|
+
changed.append(p["klipper_include"])
|
|
271
|
+
if not a.klipper_include_dir:
|
|
272
|
+
if ensure_section(p["printer_cfg"], f"include {KLIPPER_INCLUDE}",
|
|
273
|
+
f"[include {KLIPPER_INCLUDE}]\n", a.owner):
|
|
274
|
+
changed.append(p["printer_cfg"])
|
|
275
|
+
store = p["store"]
|
|
276
|
+
if not store.exists():
|
|
277
|
+
s = None
|
|
278
|
+
if a.import_ini:
|
|
279
|
+
s = cfgmod.import_ini(a.import_ini[0], a.import_ini[1],
|
|
280
|
+
*(a.import_secrets or ("", "")),
|
|
281
|
+
name=a.name)
|
|
282
|
+
if s["enabled"] or s["server"] or s["secret"]:
|
|
283
|
+
print(f"Moving settings from {a.import_ini[0]} "
|
|
284
|
+
f"[{a.import_ini[1]}] to {store.settings_path}")
|
|
285
|
+
else:
|
|
286
|
+
s = None
|
|
287
|
+
if s is not None:
|
|
288
|
+
store.save(s)
|
|
289
|
+
chown(store.settings_path, a.owner,
|
|
290
|
+
os.path.dirname(store.settings_path))
|
|
291
|
+
if os.path.exists(store.secrets_path):
|
|
292
|
+
chown(store.secrets_path, a.owner, a.data_dir)
|
|
293
|
+
changed.append(store.settings_path)
|
|
294
|
+
# Otherwise the component writes it at its first start, with the
|
|
295
|
+
# defaults for the printer it finds
|
|
296
|
+
for path in changed:
|
|
297
|
+
print(f"Updated: {path}")
|
|
298
|
+
if not changed:
|
|
299
|
+
print("Already installed; nothing changed.")
|
|
300
|
+
print(f"pushraker {__version__} is set up. Restart Moonraker and "
|
|
301
|
+
"Klipper to load it, then open the notifications page.")
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_uninstall(a: argparse.Namespace) -> int:
|
|
306
|
+
p = places(a)
|
|
307
|
+
removed = []
|
|
308
|
+
for path in (p["shim"], p["old_shim"], p["moonraker_include"],
|
|
309
|
+
p["klipper_include"]):
|
|
310
|
+
if path and remove(path):
|
|
311
|
+
removed.append(path)
|
|
312
|
+
if not p["moonraker_include"]:
|
|
313
|
+
for section in (SECTION, OLD_SECTION):
|
|
314
|
+
if drop_section(p["moonraker_conf"], section, a.owner):
|
|
315
|
+
removed.append(f"[{section}] in {p['moonraker_conf']}")
|
|
316
|
+
if not a.klipper_include_dir and drop_section(
|
|
317
|
+
p["printer_cfg"], f"include {KLIPPER_INCLUDE}", a.owner):
|
|
318
|
+
removed.append(f"[include {KLIPPER_INCLUDE}] in {p['printer_cfg']}")
|
|
319
|
+
if a.purge:
|
|
320
|
+
for path in (p["store"].settings_path, p["store"].secrets_path):
|
|
321
|
+
if remove(path):
|
|
322
|
+
removed.append(path)
|
|
323
|
+
for path in removed:
|
|
324
|
+
print(f"Removed {path}")
|
|
325
|
+
if not removed:
|
|
326
|
+
print("Nothing to remove.")
|
|
327
|
+
print("Restart Moonraker and Klipper to unload pushraker.")
|
|
328
|
+
return 0
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def cmd_status(a: argparse.Namespace) -> int:
|
|
332
|
+
p = places(a)
|
|
333
|
+
print(f"pushraker {__version__}")
|
|
334
|
+
for what, path in (("shim", p["shim"]),
|
|
335
|
+
("Moonraker section", p["moonraker_include"]
|
|
336
|
+
or p["moonraker_conf"]),
|
|
337
|
+
("Klipper macros", p["klipper_include"]),
|
|
338
|
+
("settings", p["store"].settings_path),
|
|
339
|
+
("secrets", p["store"].secrets_path)):
|
|
340
|
+
print(f" {what}: {path} "
|
|
341
|
+
f"({'present' if os.path.exists(path) else 'missing'})")
|
|
342
|
+
if p["store"].exists():
|
|
343
|
+
print(" " + cfgmod.summary(p["store"].load()))
|
|
344
|
+
return 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
348
|
+
a = parse(sys.argv[1:] if argv is None else argv)
|
|
349
|
+
return {"install": cmd_install, "uninstall": cmd_uninstall,
|
|
350
|
+
"status": cmd_status}[a.command](a)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
if __name__ == "__main__":
|
|
354
|
+
sys.exit(main())
|