capyy 0.5.3__tar.gz → 0.5.4__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.
- {capyy-0.5.3 → capyy-0.5.4}/PKG-INFO +4 -1
- {capyy-0.5.3 → capyy-0.5.4}/README.md +3 -0
- capyy-0.5.4/capy/__main__.py +541 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/_project.py +1 -1
- capyy-0.5.4/capy/_style.py +112 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/__main__.py +26 -23
- {capyy-0.5.3 → capyy-0.5.4}/capyy.egg-info/PKG-INFO +4 -1
- {capyy-0.5.3 → capyy-0.5.4}/capyy.egg-info/SOURCES.txt +1 -0
- {capyy-0.5.3 → capyy-0.5.4}/pyproject.toml +1 -1
- {capyy-0.5.3 → capyy-0.5.4}/tests/test_capy.py +19 -1
- capyy-0.5.3/capy/__main__.py +0 -324
- {capyy-0.5.3 → capyy-0.5.4}/LICENSE +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/__init__.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/_tls.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/__init__.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_base.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_homeassistant.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_hue.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_net.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_shelly.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_wiz.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/domo/_yeelight.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/network.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capy/wifi.py +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capyy.egg-info/dependency_links.txt +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capyy.egg-info/entry_points.txt +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/capyy.egg-info/top_level.txt +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/setup.cfg +0 -0
- {capyy-0.5.3 → capyy-0.5.4}/tests/test_domo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capyy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy
|
|
5
5
|
Author: Izac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -111,6 +111,9 @@ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
|
|
|
111
111
|
cy call hello nom=Izac # inside a project: uses capy.config.json
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
`capy cli` opens an interactive prompt (history, Tab completion, `cd` between projects). Output is colored in a
|
|
115
|
+
terminal and plain text / JSON in a pipe (set `NO_COLOR=1` to disable colors).
|
|
116
|
+
|
|
114
117
|
Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
|
|
115
118
|
The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
|
|
116
119
|
|
|
@@ -89,6 +89,9 @@ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
|
|
|
89
89
|
cy call hello nom=Izac # inside a project: uses capy.config.json
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
+
`capy cli` opens an interactive prompt (history, Tab completion, `cd` between projects). Output is colored in a
|
|
93
|
+
terminal and plain text / JSON in a pipe (set `NO_COLOR=1` to disable colors).
|
|
94
|
+
|
|
92
95
|
Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
|
|
93
96
|
The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
|
|
94
97
|
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# Deux commandes, installées avec capy :
|
|
2
|
+
#
|
|
3
|
+
# cy : DANS un projet capy (un dossier avec capy.config.json, ou en dessous)
|
|
4
|
+
# cy -> montre le projet (nom, mode, adresse, fichiers, scripts…)
|
|
5
|
+
# cy NOM [args…] -> lance le script NOM de "scripts" (comme npm run)
|
|
6
|
+
# cy scripts -> liste les scripts
|
|
7
|
+
# cy server / cy client -> lance un fichier de "files"
|
|
8
|
+
# cy run -> lance le serveur en fond, puis le client, puis arrête le serveur
|
|
9
|
+
# cy call ACTION [nom=valeur…] -> appelle une action du serveur du projet (local, wifi ou network)
|
|
10
|
+
#
|
|
11
|
+
# capy : tout le reste (et tout ce que fait cy)
|
|
12
|
+
# capy cli -> le mode interactif : on tape les commandes à la suite (historique, Tab)
|
|
13
|
+
# capy new NOM [mode] -> crée un dossier NOM avec capy.config.json, server.py, client.py
|
|
14
|
+
# capy init [nom] [mode] -> crée juste capy.config.json ici
|
|
15
|
+
# capy domo … -> la domotique (capy domo help)
|
|
16
|
+
# capy relay PORT [CLÉ] -> lance un relais capy.network (sur le VPS)
|
|
17
|
+
# capy call HÔTE:PORT ACTION [nom=valeur…] -> appelle une action d'un serveur capy
|
|
18
|
+
# capy wifi scan / capy wifi call NOM ACTION … -> les serveurs du wifi
|
|
19
|
+
# capy network call NOM ACTION … [--relay R --key K] / capy network relay PORT [CLÉ]
|
|
20
|
+
# capy version -> la version de capy
|
|
21
|
+
# mode : local (par défaut), wifi ou network
|
|
22
|
+
# python3 -m capy = capy (si la commande n'est pas dans le PATH)
|
|
23
|
+
|
|
24
|
+
import difflib
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import pathlib as pth
|
|
28
|
+
import re
|
|
29
|
+
import shlex
|
|
30
|
+
import socket
|
|
31
|
+
import subprocess
|
|
32
|
+
import sys
|
|
33
|
+
import threading
|
|
34
|
+
import time
|
|
35
|
+
|
|
36
|
+
from . import _style as st
|
|
37
|
+
from ._project import COMMANDS, FILE_NAME, MODES, find, init, new, project
|
|
38
|
+
|
|
39
|
+
OUTSIDE = ("init", "new", "domo", "relay", "wifi", "network") # Commandes de capy, pas de cy
|
|
40
|
+
|
|
41
|
+
# L'aide : (section, [(commande, explication)])
|
|
42
|
+
HELP = [
|
|
43
|
+
("Dans un projet capy (" + FILE_NAME + ")", [
|
|
44
|
+
("cy", "montre le projet"),
|
|
45
|
+
("cy NOM [args…]", "lance le script NOM de \"scripts\" (comme npm run)"),
|
|
46
|
+
("cy scripts", "liste les scripts"),
|
|
47
|
+
("cy server / client", "lance un fichier de \"files\""),
|
|
48
|
+
("cy run", "serveur en fond + client, puis arrête le serveur"),
|
|
49
|
+
("cy call ACTION [nom=valeur…]", "appelle une action du serveur du projet"),
|
|
50
|
+
]),
|
|
51
|
+
("Partout", [
|
|
52
|
+
("capy cli", "le mode interactif (historique ↑↓, complétion Tab)"),
|
|
53
|
+
("capy new NOM [mode]", "crée un projet complet (config, server.py, client.py)"),
|
|
54
|
+
("capy init [nom] [mode]", f"crée juste {FILE_NAME} ici"),
|
|
55
|
+
("capy call HÔTE:PORT ACTION [nom=valeur…]", "appelle une action (--no-tls pour un serveur en clair)"),
|
|
56
|
+
("capy wifi scan", "les serveurs capy du wifi"),
|
|
57
|
+
("capy wifi call NOM ACTION [nom=valeur…]", "appelle un serveur du wifi"),
|
|
58
|
+
("capy network call NOM ACTION [nom=valeur…]", "par internet (--relay HÔTE:PORT --key CLÉ)"),
|
|
59
|
+
("capy relay PORT [CLÉ]", "lance un relais capy.network"),
|
|
60
|
+
("capy domo …", "la domotique (capy domo help)"),
|
|
61
|
+
("capy version", "la version de capy"),
|
|
62
|
+
]),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def usage():
|
|
67
|
+
lines = [st.title("capy", f"v{version()} · le réseau facile en Python"), ""]
|
|
68
|
+
width = max(len(c) for _, commands in HELP for c, _ in commands) # Les deux sections alignées ensemble
|
|
69
|
+
for name, commands in HELP:
|
|
70
|
+
lines.append(st.section(name))
|
|
71
|
+
lines += [f" {st.code(c)}{' ' * (width - len(c) + 3)}{st.paint(d, 'dim')}" for c, d in commands]
|
|
72
|
+
lines.append("")
|
|
73
|
+
lines.append(st.paint(f" Paramètres : nom=Izac age=14 actif=true liste=[1,2] · modes : {', '.join(MODES)}", "dim"))
|
|
74
|
+
return "\n".join(lines)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# La version de capy : celle du pyproject.toml si on tourne depuis le dossier du code (installation éditable,
|
|
78
|
+
# où pip garde la version du jour de l'installation), sinon celle installée par pip
|
|
79
|
+
def version():
|
|
80
|
+
pyproject = pth.Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
81
|
+
if pyproject.is_file():
|
|
82
|
+
match = re.search(r'^version = "(.+)"', pyproject.read_text(encoding="utf-8"), re.M)
|
|
83
|
+
if match and 'name = "capyy"' in pyproject.read_text(encoding="utf-8"):
|
|
84
|
+
return match.group(1)
|
|
85
|
+
try:
|
|
86
|
+
from importlib.metadata import version as package_version
|
|
87
|
+
return package_version("capyy")
|
|
88
|
+
except Exception:
|
|
89
|
+
return "?"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Le projet du dossier actuel (ou au-dessus), sinon on explique quoi faire
|
|
93
|
+
def _project():
|
|
94
|
+
path = find(".")
|
|
95
|
+
if path is None:
|
|
96
|
+
sys.exit(f"Pas de {FILE_NAME} ici ni au-dessus : ce dossier n'est pas un projet capy.\n"
|
|
97
|
+
f" Crée un projet : capy new mon-projet\n"
|
|
98
|
+
f" ou juste le fichier : capy init")
|
|
99
|
+
try:
|
|
100
|
+
return project(path)
|
|
101
|
+
except ValueError as e:
|
|
102
|
+
sys.exit(str(e))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# Le projet, joliment (cy sans rien)
|
|
106
|
+
def describe(app):
|
|
107
|
+
head = st.paint(app.name, bold=True) + (st.paint(f" v{app.version}", "dim") if app.version else "") \
|
|
108
|
+
+ (st.paint(f" par {app.author}", "dim") if app.author else "")
|
|
109
|
+
lines = [f"{st.paint('◆', 'mauve', bold=True)} {head}"]
|
|
110
|
+
if app.description:
|
|
111
|
+
lines.append(f" {app.description}")
|
|
112
|
+
where = {"local": f"{app.host}:{app.port}", "wifi": f"sur le wifi, nom {app.name!r}",
|
|
113
|
+
"network": f"relais {app.relay}, nom {app.name!r}"}[app.mode]
|
|
114
|
+
lock = st.paint("🔒 chiffré", "green") if app.tls else st.paint("en clair", "yellow")
|
|
115
|
+
lines += ["", st.table([(st.paint("Mode", "dim"), f"{st.paint(app.mode, 'mauve')} · {where} · {lock}"),
|
|
116
|
+
(st.paint("Fichier", "dim"), st.paint(str(app.path).replace(str(pth.Path.home()), "~"), "dim"))])]
|
|
117
|
+
if app.files:
|
|
118
|
+
lines += ["", st.section(" Fichiers")]
|
|
119
|
+
rows = []
|
|
120
|
+
for name, path in app.files.items():
|
|
121
|
+
shown = path.relative_to(app.folder) if str(path).startswith(str(app.folder)) else path
|
|
122
|
+
mark = st.paint("✓", "green") if path.is_file() else st.paint("✗", "red")
|
|
123
|
+
rows.append((mark, st.code(name), str(shown) + ("" if path.is_file() else st.paint(" introuvable", "red"))))
|
|
124
|
+
lines.append(st.table(rows, indent=4, gap=2))
|
|
125
|
+
if app.scripts:
|
|
126
|
+
lines += ["", st.section(" Scripts") + st.paint(" cy NOM pour en lancer un", "dim")]
|
|
127
|
+
lines.append(st.table([(st.code(name), command) for name, command in app.scripts.items()], indent=4))
|
|
128
|
+
return "\n".join(lines)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# Sortir les options "--nom valeur" (et "--drapeau") d'une liste d'arguments -> (options, reste)
|
|
132
|
+
def _options(args, flags=(), values=()):
|
|
133
|
+
options, rest, i = {}, [], 0
|
|
134
|
+
while i < len(args):
|
|
135
|
+
word = args[i]
|
|
136
|
+
if word in flags:
|
|
137
|
+
options[word] = True
|
|
138
|
+
elif word in values:
|
|
139
|
+
if i + 1 >= len(args):
|
|
140
|
+
sys.exit(f"capy : il manque une valeur après {word}")
|
|
141
|
+
options[word] = args[i + 1]
|
|
142
|
+
i += 1
|
|
143
|
+
elif word.startswith("--"):
|
|
144
|
+
sys.exit(f"capy : option {word} inconnue (possibles : {', '.join((*flags, *values)) or 'aucune'})")
|
|
145
|
+
else:
|
|
146
|
+
rest.append(word)
|
|
147
|
+
i += 1
|
|
148
|
+
return options, rest
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# "nom=Izac" "age=14" -> {"nom": "Izac", "age": 14} : une valeur qui est du JSON (14, true, [1, 2]) est convertie
|
|
152
|
+
def _params(words):
|
|
153
|
+
params = {}
|
|
154
|
+
for word in words:
|
|
155
|
+
key, sep, value = word.partition("=")
|
|
156
|
+
if not sep or not key:
|
|
157
|
+
sys.exit(f"capy : paramètre {word!r} invalide, écris nom=valeur (ex : nom=Izac age=14)")
|
|
158
|
+
try:
|
|
159
|
+
params[key] = json.loads(value)
|
|
160
|
+
except ValueError:
|
|
161
|
+
params[key] = value
|
|
162
|
+
return params
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Faire l'appel et afficher la réponse en JSON (coloré dans un terminal) ; code 1 si erreur (pratique dans un script)
|
|
166
|
+
def _call(function):
|
|
167
|
+
import capy
|
|
168
|
+
started = time.monotonic()
|
|
169
|
+
try:
|
|
170
|
+
answer = function()
|
|
171
|
+
except (ConnectionError, ValueError, KeyError, capy.SecurityError) as e:
|
|
172
|
+
sys.exit(str(e).strip("'\""))
|
|
173
|
+
print(st.pretty(answer))
|
|
174
|
+
if st.enabled():
|
|
175
|
+
print(st.paint(f" {(time.monotonic() - started) * 1000:.0f} ms", "dim"))
|
|
176
|
+
if isinstance(answer, dict) and "erreur" in answer:
|
|
177
|
+
sys.exit(1)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
USAGE_CALL = "Usage : capy call HÔTE:PORT ACTION [nom=valeur…] [--no-tls] (ex : capy call localhost:4000 hello nom=Izac)"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# capy call localhost:4000 hello nom=Izac
|
|
184
|
+
def call_direct(args):
|
|
185
|
+
import capy
|
|
186
|
+
options, rest = _options(args, flags=("--no-tls",))
|
|
187
|
+
if len(rest) < 2:
|
|
188
|
+
sys.exit(USAGE_CALL)
|
|
189
|
+
host, _, port = rest[0].rpartition(":")
|
|
190
|
+
if not host or not port.isdigit():
|
|
191
|
+
sys.exit(f"capy : adresse {rest[0]!r} invalide, écris HÔTE:PORT (ex : localhost:4000)\n{USAGE_CALL}")
|
|
192
|
+
_call(lambda: capy.call(host, int(port), rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# capy wifi scan / capy wifi call maison hello nom=Izac
|
|
196
|
+
def wifi(args):
|
|
197
|
+
import capy.wifi
|
|
198
|
+
sub = args[0] if args else None
|
|
199
|
+
if sub == "scan":
|
|
200
|
+
options, _ = _options(args[1:], values=("--timeout",))
|
|
201
|
+
if st.enabled():
|
|
202
|
+
print(st.paint(" Recherche sur le wifi…", "dim"), flush=True)
|
|
203
|
+
found = capy.wifi.scan(timeout=float(options.get("--timeout", 1)))
|
|
204
|
+
if not found:
|
|
205
|
+
sys.exit("Aucun serveur capy.wifi trouvé (même wifi ? wifi public qui isole les appareils ?)")
|
|
206
|
+
print(st.table([(st.paint("●", "green"), st.code(name), f"{ip}:{port}",
|
|
207
|
+
st.paint("cet appareil", "dim") if ip == "127.0.0.1" else "")
|
|
208
|
+
for name, (ip, port) in sorted(found.items())]))
|
|
209
|
+
elif sub == "call":
|
|
210
|
+
options, rest = _options(args[1:], flags=("--no-tls",))
|
|
211
|
+
if len(rest) < 2:
|
|
212
|
+
sys.exit("Usage : capy wifi call NOM ACTION [nom=valeur…] [--no-tls] (ex : capy wifi call maison hello)")
|
|
213
|
+
_call(lambda: capy.wifi.call(rest[0], rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
|
|
214
|
+
else:
|
|
215
|
+
sys.exit("Usage : capy wifi scan [--timeout S] ou capy wifi call NOM ACTION [nom=valeur…] [--no-tls]")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# capy network call maison hello --relay mon-vps.fr:3879 --key … / capy network relay 3879 CLÉ
|
|
219
|
+
# Relais et clé : options, sinon le capy.config.json du dossier (mode network), sinon $CAPY_RELAY / $CAPY_KEY
|
|
220
|
+
def network(args):
|
|
221
|
+
import capy.network
|
|
222
|
+
sub = args[0] if args else None
|
|
223
|
+
if sub == "relay" or (sub or "").isdigit(): # capy network relay 3879 CLÉ (ou l'ancien capy network 3879 CLÉ)
|
|
224
|
+
capy.network.cli(args[1:] if sub == "relay" else args)
|
|
225
|
+
elif sub == "call":
|
|
226
|
+
options, rest = _options(args[1:], values=("--relay", "--key", "--fingerprint"))
|
|
227
|
+
if len(rest) < 2:
|
|
228
|
+
sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay HÔTE:PORT] [--key CLÉ] [--fingerprint …]")
|
|
229
|
+
relay, key, fingerprint = options.get("--relay"), options.get("--key"), options.get("--fingerprint")
|
|
230
|
+
path = find(".")
|
|
231
|
+
if path and not relay:
|
|
232
|
+
try:
|
|
233
|
+
app = project(path)
|
|
234
|
+
except ValueError:
|
|
235
|
+
app = None
|
|
236
|
+
if app and app.mode == "network":
|
|
237
|
+
relay, key, fingerprint = app.relay, key or app.key, fingerprint or app.fingerprint
|
|
238
|
+
relay, key = relay or os.environ.get("CAPY_RELAY"), key or os.environ.get("CAPY_KEY")
|
|
239
|
+
if not relay:
|
|
240
|
+
sys.exit("capy : quel relais ? Ajoute --relay mon-vps.fr:3879 (ou un capy.config.json en mode network, ou $CAPY_RELAY)")
|
|
241
|
+
capy.network.use(relay, key=key, fingerprint=fingerprint)
|
|
242
|
+
_call(lambda: capy.network.call(rest[0], rest[1], **_params(rest[2:])))
|
|
243
|
+
else:
|
|
244
|
+
sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay …] [--key …] ou capy network relay PORT [CLÉ]")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Lancer un fichier du projet avec le même Python, depuis le dossier du projet
|
|
248
|
+
def _start(app, name, args=(), **options):
|
|
249
|
+
try:
|
|
250
|
+
path = app.file(name)
|
|
251
|
+
except KeyError as e:
|
|
252
|
+
sys.exit(e.args[0])
|
|
253
|
+
if not path.is_file():
|
|
254
|
+
sys.exit(f"capy : le fichier {name!r} ({path}) n'existe pas")
|
|
255
|
+
return subprocess.Popen([sys.executable, "-u", str(path), *args], cwd=app.folder, **options)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# Attendre que le serveur réponde (10 s max) ; False s'il s'est arrêté avant
|
|
259
|
+
def _wait_ready(app, process):
|
|
260
|
+
end = time.monotonic() + 10
|
|
261
|
+
while time.monotonic() < end:
|
|
262
|
+
if process.poll() is not None:
|
|
263
|
+
return False
|
|
264
|
+
try:
|
|
265
|
+
if app.mode == "local" and app.port:
|
|
266
|
+
socket.create_connection((app.host, app.port), timeout=0.3).close()
|
|
267
|
+
return True
|
|
268
|
+
if app.mode == "wifi":
|
|
269
|
+
import capy.wifi
|
|
270
|
+
capy.wifi._found.pop(app.name, None)
|
|
271
|
+
capy.wifi.find(app.name, timeout=0.3)
|
|
272
|
+
return True
|
|
273
|
+
if app.mode == "network" or not app.port:
|
|
274
|
+
time.sleep(2) # Pas de moyen simple de vérifier : on laisse 2 s au serveur
|
|
275
|
+
return process.poll() is None
|
|
276
|
+
except (OSError, ConnectionError):
|
|
277
|
+
time.sleep(0.2)
|
|
278
|
+
return process.poll() is None
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Recopier la sortie du serveur, avec "server │" devant chaque ligne
|
|
282
|
+
def _relay_output(process):
|
|
283
|
+
for line in process.stdout:
|
|
284
|
+
print(f"{st.paint('server │', 'dim')} {line}", end="", flush=True)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def run(app, args):
|
|
288
|
+
server = _start(app, "server", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
289
|
+
threading.Thread(target=_relay_output, args=(server,), daemon=True).start()
|
|
290
|
+
try:
|
|
291
|
+
if not _wait_ready(app, server):
|
|
292
|
+
time.sleep(0.2) # Laisser s'afficher l'erreur du serveur
|
|
293
|
+
sys.exit("capy : le serveur s'est arrêté (ou ne répond pas après 10 s)")
|
|
294
|
+
client = _start(app, "client", args)
|
|
295
|
+
code = client.wait()
|
|
296
|
+
except KeyboardInterrupt:
|
|
297
|
+
code = 130
|
|
298
|
+
finally:
|
|
299
|
+
server.terminate()
|
|
300
|
+
try:
|
|
301
|
+
server.wait(timeout=3)
|
|
302
|
+
except subprocess.TimeoutExpired:
|
|
303
|
+
server.kill()
|
|
304
|
+
sys.exit(code)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# prog : la commande tapée (capy ou cy), pour que les suggestions la reprennent
|
|
308
|
+
def main(args, prog="capy"):
|
|
309
|
+
command = args[0] if args else None
|
|
310
|
+
if command in ("-h", "--help", "help"):
|
|
311
|
+
print(usage())
|
|
312
|
+
return
|
|
313
|
+
if command in ("version", "--version", "-V"):
|
|
314
|
+
print(f"capy {version()}")
|
|
315
|
+
return
|
|
316
|
+
if command == "cli":
|
|
317
|
+
interactive()
|
|
318
|
+
return
|
|
319
|
+
if command == "domo": # capy domo salon on
|
|
320
|
+
from .domo.__main__ import cli as domo_cli
|
|
321
|
+
domo_cli(args[1:])
|
|
322
|
+
return
|
|
323
|
+
if command == "relay": # capy relay 3879 CLÉ
|
|
324
|
+
from .network import cli as relay_cli
|
|
325
|
+
relay_cli(args[1:])
|
|
326
|
+
return
|
|
327
|
+
if command == "network":
|
|
328
|
+
network(args[1:])
|
|
329
|
+
return
|
|
330
|
+
if command == "wifi":
|
|
331
|
+
wifi(args[1:])
|
|
332
|
+
return
|
|
333
|
+
if command == "call" and len(args) > 1 and ":" in args[1]: # capy call localhost:4000 hello
|
|
334
|
+
call_direct(args[1:])
|
|
335
|
+
return
|
|
336
|
+
if command in ("init", "new"):
|
|
337
|
+
rest = args[1:]
|
|
338
|
+
mode = next((a for a in rest if a in MODES), "local")
|
|
339
|
+
name = next((a for a in rest if a not in MODES), None)
|
|
340
|
+
if command == "new" and not name:
|
|
341
|
+
sys.exit("Usage : capy new NOM [mode]")
|
|
342
|
+
try:
|
|
343
|
+
path = new(name, mode) / FILE_NAME if command == "new" else init(name=name, mode=mode)
|
|
344
|
+
except (FileExistsError, ValueError) as e:
|
|
345
|
+
sys.exit(str(e))
|
|
346
|
+
print(st.ok(f"Créé : {path.parent if command == 'new' else path}"), "\n")
|
|
347
|
+
print(describe(project(path)))
|
|
348
|
+
if command == "new":
|
|
349
|
+
print(f"\n{st.section('Pour l’essayer')}\n {st.code(f'cd {name}')}\n {st.code('cy run')}")
|
|
350
|
+
if mode == "network":
|
|
351
|
+
print(st.paint("\nPense à : export CAPY_KEY='la clé du relais' (dans ~/.zshrc), et à mettre l'adresse de ton relais", "yellow"))
|
|
352
|
+
return
|
|
353
|
+
if command is None and prog == "capy" and find(".") is None: # capy tout seul, hors d'un projet : l'aide
|
|
354
|
+
print(usage())
|
|
355
|
+
return
|
|
356
|
+
app = _project()
|
|
357
|
+
if command is None:
|
|
358
|
+
print(describe(app))
|
|
359
|
+
elif command == "run":
|
|
360
|
+
run(app, args[1:])
|
|
361
|
+
elif command == "call": # cy call hello nom=Izac : le serveur du projet, quel que soit son mode
|
|
362
|
+
if len(args) < 2:
|
|
363
|
+
sys.exit("Usage : cy call ACTION [nom=valeur…] (ex : cy call hello nom=Izac)")
|
|
364
|
+
_call(lambda: app.call(args[1], **_params(args[2:])))
|
|
365
|
+
elif command == "scripts":
|
|
366
|
+
if not app.scripts:
|
|
367
|
+
print('Pas de "scripts" dans ' + FILE_NAME + ', ex : "scripts": {"test": "python3 -m unittest"}')
|
|
368
|
+
print(st.table([(st.code(name), line) for name, line in app.scripts.items()]))
|
|
369
|
+
elif command in app.scripts:
|
|
370
|
+
print(st.paint(f"> {app.scripts[command]}", "dim"), flush=True) # Comme npm : on montre la commande lancée
|
|
371
|
+
try:
|
|
372
|
+
sys.exit(app.run_script(command, args[1:]))
|
|
373
|
+
except KeyboardInterrupt:
|
|
374
|
+
sys.exit(130)
|
|
375
|
+
elif command in app.files:
|
|
376
|
+
try:
|
|
377
|
+
sys.exit(_start(app, command, args[1:]).wait())
|
|
378
|
+
except KeyboardInterrupt:
|
|
379
|
+
sys.exit(130)
|
|
380
|
+
else:
|
|
381
|
+
names = list(app.scripts) + list(app.files) + [c for c in COMMANDS if prog == "capy" or c not in OUTSIDE]
|
|
382
|
+
close = difflib.get_close_matches(command, names, n=1)
|
|
383
|
+
if close:
|
|
384
|
+
sys.exit(f"{prog} : {command!r} inconnu, tu voulais dire {close[0]!r} ? ({prog} {close[0]})")
|
|
385
|
+
sys.exit(f"{prog} : commande, script ou fichier {command!r} inconnu (possibles : {', '.join(names)})")
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ============================================================
|
|
389
|
+
# MODE INTERACTIF (capy cli)
|
|
390
|
+
# ============================================================
|
|
391
|
+
|
|
392
|
+
HISTORY = pth.Path.home() / ".capy" / "history"
|
|
393
|
+
SUBCOMMANDS = {"wifi": ["scan", "call"], "network": ["call", "relay"], "domo": ["scan", "pair", "list", "scene", "serve", "help"]}
|
|
394
|
+
BUILTINS = ["help", "exit", "clear", "cd", "version"]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# Les mots proposés par Tab : commandes, scripts et fichiers du projet, sous-commandes
|
|
398
|
+
def _words(before):
|
|
399
|
+
words = before.split()
|
|
400
|
+
if not words or (len(words) == 1 and not before.endswith(" ")):
|
|
401
|
+
names = set(COMMANDS) | set(BUILTINS)
|
|
402
|
+
path = find(".")
|
|
403
|
+
if path:
|
|
404
|
+
try:
|
|
405
|
+
app = project(path)
|
|
406
|
+
names |= set(app.scripts) | set(app.files)
|
|
407
|
+
except ValueError:
|
|
408
|
+
pass
|
|
409
|
+
return sorted(names - {"cli"})
|
|
410
|
+
if words[0] in SUBCOMMANDS and (len(words) == 1 or (len(words) == 2 and not before.endswith(" "))):
|
|
411
|
+
return SUBCOMMANDS[words[0]]
|
|
412
|
+
if words[0] == "cd":
|
|
413
|
+
return sorted(p.name + "/" for p in pth.Path(".").iterdir() if p.is_dir() and not p.name.startswith("."))
|
|
414
|
+
return []
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _setup_readline():
|
|
418
|
+
try:
|
|
419
|
+
import readline
|
|
420
|
+
except ImportError: # Windows sans pyreadline : pas d'historique, tout le reste marche
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
def complete(text, state):
|
|
424
|
+
before = readline.get_line_buffer()[:readline.get_begidx()]
|
|
425
|
+
options = [w for w in _words(before + text) if w.startswith(text)]
|
|
426
|
+
return options[state] + ("" if options[state].endswith("/") else " ") if state < len(options) else None
|
|
427
|
+
|
|
428
|
+
readline.set_completer(complete)
|
|
429
|
+
readline.set_completer_delims(" ")
|
|
430
|
+
if "libedit" in (readline.__doc__ or ""): # macOS : libedit au lieu de GNU readline
|
|
431
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
|
432
|
+
else:
|
|
433
|
+
readline.parse_and_bind("tab: complete")
|
|
434
|
+
try:
|
|
435
|
+
readline.read_history_file(HISTORY)
|
|
436
|
+
except OSError:
|
|
437
|
+
pass
|
|
438
|
+
readline.set_history_length(1000)
|
|
439
|
+
return readline
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# L'invite : "capy (mon-projet) ❯" ; les couleurs entourées de \001 \002 pour que readline compte bien la largeur
|
|
443
|
+
def _prompt():
|
|
444
|
+
path = find(".")
|
|
445
|
+
name = None
|
|
446
|
+
if path:
|
|
447
|
+
try:
|
|
448
|
+
name = project(path).name
|
|
449
|
+
except ValueError:
|
|
450
|
+
name = path.parent.name
|
|
451
|
+
if not st.enabled() or not sys.stdin.isatty():
|
|
452
|
+
return ""
|
|
453
|
+
text = st.paint("capy", "mauve", bold=True) + (st.paint(f" ({name})", "dim") if name else "") + st.paint(" ❯ ", "pink", bold=True)
|
|
454
|
+
return "".join(f"\001{part}\002" if part.startswith("\033") else part for part in re.split(r"(\033\[[0-9;]*m)", text))
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
# capy cli : on tape les commandes à la suite, sans retaper capy (exit ou Ctrl+D pour sortir)
|
|
458
|
+
def interactive():
|
|
459
|
+
readline = _setup_readline() if sys.stdin.isatty() else None
|
|
460
|
+
if st.enabled() and sys.stdin.isatty():
|
|
461
|
+
print(st.title("capy", f"v{version()} · mode interactif"))
|
|
462
|
+
print(st.paint(" help pour l'aide · Tab pour compléter · ↑↓ l'historique · exit pour sortir\n", "dim"))
|
|
463
|
+
while True:
|
|
464
|
+
try:
|
|
465
|
+
line = input(_prompt())
|
|
466
|
+
except EOFError:
|
|
467
|
+
print()
|
|
468
|
+
break
|
|
469
|
+
except KeyboardInterrupt:
|
|
470
|
+
print()
|
|
471
|
+
continue
|
|
472
|
+
try:
|
|
473
|
+
words = shlex.split(line)
|
|
474
|
+
except ValueError as e:
|
|
475
|
+
print(st.bad(f"ligne illisible : {e}", sys.stderr), file=sys.stderr)
|
|
476
|
+
continue
|
|
477
|
+
if words[:1] in (["capy"], ["cy"]): # L'habitude de taper capy devant
|
|
478
|
+
words = words[1:]
|
|
479
|
+
if not words:
|
|
480
|
+
continue
|
|
481
|
+
if words[0] in ("exit", "quit", "q"):
|
|
482
|
+
break
|
|
483
|
+
if words[0] == "clear":
|
|
484
|
+
print("\033[2J\033[H", end="")
|
|
485
|
+
continue
|
|
486
|
+
if words[0] == "cd":
|
|
487
|
+
try:
|
|
488
|
+
os.chdir(os.path.expanduser(words[1] if len(words) > 1 else "~"))
|
|
489
|
+
except OSError as e:
|
|
490
|
+
print(st.bad(str(e), sys.stderr), file=sys.stderr)
|
|
491
|
+
continue
|
|
492
|
+
if words[0] == "cli":
|
|
493
|
+
continue
|
|
494
|
+
try:
|
|
495
|
+
main(words)
|
|
496
|
+
except SystemExit as e:
|
|
497
|
+
_show_error(e)
|
|
498
|
+
except KeyboardInterrupt:
|
|
499
|
+
print()
|
|
500
|
+
if readline:
|
|
501
|
+
try:
|
|
502
|
+
HISTORY.parent.mkdir(exist_ok=True)
|
|
503
|
+
readline.write_history_file(HISTORY)
|
|
504
|
+
except OSError:
|
|
505
|
+
pass
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
# Une erreur (sys.exit("message")) : ✗ rouge devant, sur la sortie d'erreur
|
|
509
|
+
def _show_error(error):
|
|
510
|
+
if isinstance(error.code, str):
|
|
511
|
+
first, *rest = error.code.split("\n")
|
|
512
|
+
print("\n".join([st.bad(first, sys.stderr), *rest]), file=sys.stderr)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _run(args, prog):
|
|
516
|
+
try:
|
|
517
|
+
main(args, prog)
|
|
518
|
+
except SystemExit as e:
|
|
519
|
+
_show_error(e)
|
|
520
|
+
raise SystemExit(1 if isinstance(e.code, str) else e.code) from None
|
|
521
|
+
except KeyboardInterrupt:
|
|
522
|
+
print()
|
|
523
|
+
raise SystemExit(130) from None
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# La commande cy : seulement les commandes d'un projet (et cy cli)
|
|
527
|
+
def cli():
|
|
528
|
+
args = sys.argv[1:]
|
|
529
|
+
if args[:1] in [[c] for c in OUTSIDE]:
|
|
530
|
+
_show_error(SystemExit(f"cy sert dans un projet capy ; pour ça, c'est la commande capy : capy {' '.join(args)}"))
|
|
531
|
+
raise SystemExit(1)
|
|
532
|
+
_run(args, "cy")
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# La commande capy (et python3 -m capy) : tout
|
|
536
|
+
def capy_cli():
|
|
537
|
+
_run(sys.argv[1:], "capy")
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
if __name__ == "__main__":
|
|
541
|
+
capy_cli()
|
|
@@ -46,7 +46,7 @@ FIELDS = {
|
|
|
46
46
|
|
|
47
47
|
EXAMPLE = {"name": "mon-projet", "mode": "local", "host": "localhost", "port": 4000, "tls": True,
|
|
48
48
|
"files": {"server": "server.py", "client": "client.py"}}
|
|
49
|
-
COMMANDS = ("init", "new", "run", "scripts", "help", "call", "domo", "relay", "wifi", "network") # Mots réservés de cy / capy (pas des noms de fichiers ni de scripts)
|
|
49
|
+
COMMANDS = ("init", "new", "run", "scripts", "help", "call", "cli", "version", "domo", "relay", "wifi", "network") # Mots réservés de cy / capy (pas des noms de fichiers ni de scripts)
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
# Dossier du script lancé (comme simplesuit.json) ; sans script (python3 -c, REPL) -> dossier actuel
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# capy : le style des commandes capy et cy (couleurs, icônes, JSON coloré), sans dépendance
|
|
2
|
+
# Les couleurs se coupent toutes seules si la sortie n'est pas un terminal (fichier, | jq…) ou avec NO_COLOR=1
|
|
3
|
+
|
|
4
|
+
import json as jsonlib
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
# Couleurs Catppuccin Mocha (comme le thème du terminal), en RGB 24 bits
|
|
10
|
+
PALETTE = {
|
|
11
|
+
"text": (205, 214, 244), "dim": (127, 132, 156), "blue": (137, 180, 250), "green": (166, 227, 161),
|
|
12
|
+
"red": (243, 139, 168), "peach": (250, 179, 135), "yellow": (249, 226, 175), "mauve": (203, 166, 247),
|
|
13
|
+
"teal": (148, 226, 213), "pink": (245, 194, 231),
|
|
14
|
+
}
|
|
15
|
+
RESET = "\033[0m"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Couleurs sur cette sortie ? (terminal, et pas de NO_COLOR)
|
|
19
|
+
def enabled(stream=None):
|
|
20
|
+
stream = stream or sys.stdout
|
|
21
|
+
return hasattr(stream, "isatty") and stream.isatty() and not os.environ.get("NO_COLOR")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Colorer un texte : paint("ok", "green", bold=True)
|
|
25
|
+
def paint(text, color=None, bold=False, dim=False, stream=None):
|
|
26
|
+
if not enabled(stream):
|
|
27
|
+
return str(text)
|
|
28
|
+
codes = ""
|
|
29
|
+
if color:
|
|
30
|
+
codes += "\033[38;2;{};{};{}m".format(*PALETTE[color])
|
|
31
|
+
if bold:
|
|
32
|
+
codes += "\033[1m"
|
|
33
|
+
if dim:
|
|
34
|
+
codes += "\033[2m"
|
|
35
|
+
return f"{codes}{text}{RESET}"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Enlever les couleurs (pour mesurer la vraie largeur d'un texte)
|
|
39
|
+
def strip(text):
|
|
40
|
+
return re.sub(r"\033\[[0-9;]*m", "", text)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# Raccourcis
|
|
44
|
+
def ok(text):
|
|
45
|
+
return f"{paint('✓', 'green', bold=True)} {text}"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def bad(text, stream=None):
|
|
49
|
+
return f"{paint('✗', 'red', bold=True, stream=stream)} {text}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def title(text, extra=""):
|
|
53
|
+
return f"{paint('◆', 'mauve', bold=True)} {paint(text, bold=True)}" + (f" {paint(extra, 'dim')}" if extra else "")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def section(text):
|
|
57
|
+
return paint(text, "blue", bold=True)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def code(text):
|
|
61
|
+
return paint(text, "teal")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Un petit carré de la couleur donnée ("#FF7800") : l'aperçu d'une lampe
|
|
65
|
+
def swatch(hexa, stream=None):
|
|
66
|
+
if not enabled(stream) or not hexa:
|
|
67
|
+
return ""
|
|
68
|
+
r, g, b = (int(hexa.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4))
|
|
69
|
+
return f"\033[38;2;{r};{g};{b}m██{RESET}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Un tableau aligné : [(colonne1, colonne2, …)] ; les couleurs ne gênent pas l'alignement
|
|
73
|
+
def table(rows, indent=2, gap=3):
|
|
74
|
+
rows = [[str(c) for c in row] for row in rows]
|
|
75
|
+
if not rows:
|
|
76
|
+
return ""
|
|
77
|
+
widths = [max(len(strip(row[i])) for row in rows if i < len(row)) for i in range(max(len(r) for r in rows))]
|
|
78
|
+
lines = []
|
|
79
|
+
for row in rows:
|
|
80
|
+
cells = [cell + " " * (widths[i] - len(strip(cell))) if i < len(row) - 1 else cell for i, cell in enumerate(row)]
|
|
81
|
+
lines.append(" " * indent + (" " * gap).join(cells).rstrip())
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# Du JSON indenté et coloré (clés en bleu, textes en vert, nombres en orange…), JSON pur sans couleurs
|
|
86
|
+
def pretty(data, stream=None):
|
|
87
|
+
if not enabled(stream):
|
|
88
|
+
return jsonlib.dumps(data, indent=2, ensure_ascii=False)
|
|
89
|
+
return _pretty(data, 0)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _pretty(value, level):
|
|
93
|
+
pad, end = " " * (level + 1), " " * level
|
|
94
|
+
punct = lambda t: paint(t, "dim")
|
|
95
|
+
if isinstance(value, dict):
|
|
96
|
+
if not value:
|
|
97
|
+
return punct("{}")
|
|
98
|
+
items = [f"{pad}{paint(jsonlib.dumps(str(k), ensure_ascii=False), 'blue')}{punct(':')} {_pretty(v, level + 1)}"
|
|
99
|
+
for k, v in value.items()]
|
|
100
|
+
return punct("{") + "\n" + (punct(",") + "\n").join(items) + "\n" + end + punct("}")
|
|
101
|
+
if isinstance(value, list):
|
|
102
|
+
if not value:
|
|
103
|
+
return punct("[]")
|
|
104
|
+
items = [pad + _pretty(v, level + 1) for v in value]
|
|
105
|
+
return punct("[") + "\n" + (punct(",") + "\n").join(items) + "\n" + end + punct("]")
|
|
106
|
+
if value is None:
|
|
107
|
+
return paint("null", "red")
|
|
108
|
+
if isinstance(value, bool):
|
|
109
|
+
return paint("true" if value else "false", "mauve")
|
|
110
|
+
if isinstance(value, (int, float)):
|
|
111
|
+
return paint(jsonlib.dumps(value), "peach")
|
|
112
|
+
return paint(jsonlib.dumps(str(value), ensure_ascii=False), "green")
|
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
import time
|
|
17
17
|
|
|
18
18
|
import capy
|
|
19
|
+
from capy import _style as st
|
|
19
20
|
|
|
20
21
|
from . import FILE_NAME, WHITES, Home, Remote, _hue, find, serve, to_kelvin, to_rgb
|
|
21
22
|
|
|
@@ -51,18 +52,21 @@ def parse(words):
|
|
|
51
52
|
return changes
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
# Une ligne d'état : "● salon allumé · 30 % · ██ #FF7800" (le carré est de la couleur de la lampe)
|
|
54
56
|
def show_state(name, state):
|
|
57
|
+
label = st.code(f"{name:<18}")
|
|
55
58
|
if not state.get("reachable", True):
|
|
56
|
-
return f" {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
return f" {st.paint('✗', 'red')} {label} {st.paint('injoignable', 'red')}"
|
|
60
|
+
if not state["on"]:
|
|
61
|
+
return f" {st.paint('○', 'dim')} {label} {st.paint('éteint', 'dim')}"
|
|
62
|
+
parts = ["allumé"]
|
|
63
|
+
if state.get("brightness") is not None:
|
|
64
|
+
parts.append(f"{state['brightness']} %")
|
|
65
|
+
if state.get("color"):
|
|
66
|
+
parts.append(f"{st.swatch(state['color'])} {state['color']}".strip())
|
|
67
|
+
if state.get("kelvin"):
|
|
68
|
+
parts.append(f"{state['kelvin']} K")
|
|
69
|
+
return f" {st.paint('●', 'yellow')} {label} {' · '.join(parts)}"
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def main(args):
|
|
@@ -81,14 +85,14 @@ def main(args):
|
|
|
81
85
|
command, rest = args[0], args[1:]
|
|
82
86
|
|
|
83
87
|
if command == "scan" and not remote:
|
|
84
|
-
print("Recherche sur le réseau…")
|
|
88
|
+
print(st.paint(" Recherche sur le réseau…", "dim"), flush=True)
|
|
85
89
|
found = home.scan()
|
|
86
90
|
for device in found:
|
|
87
|
-
print(f" + {device.name:<18} {device.brand:<14} {device.host or device.id}")
|
|
91
|
+
print(f" {st.paint('+', 'green', bold=True)} {st.code(f'{device.name:<18}')} {device.brand:<14} {st.paint(device.host or device.id, 'dim')}")
|
|
88
92
|
for hint in home.hints:
|
|
89
|
-
print(" !
|
|
93
|
+
print(f" {st.paint('!', 'yellow', bold=True)} {hint}")
|
|
90
94
|
if found:
|
|
91
|
-
print(f"{len(found)} appareil(s) ajouté(s) dans {home.save()} (renomme-les dans ce fichier si tu veux)")
|
|
95
|
+
print(st.ok(f"{len(found)} appareil(s) ajouté(s) dans {home.save()}") + st.paint(" (renomme-les dans ce fichier si tu veux)", "dim"))
|
|
92
96
|
elif not home.hints:
|
|
93
97
|
print("Rien de nouveau trouvé (même réseau ? appareils allumés ?)")
|
|
94
98
|
|
|
@@ -120,8 +124,8 @@ def main(args):
|
|
|
120
124
|
else:
|
|
121
125
|
sys.exit("Usage : capy domo pair hue [IP] ou pair homeassistant URL")
|
|
122
126
|
for device in added:
|
|
123
|
-
print(f" + {device.name:<18} {device.brand}")
|
|
124
|
-
print(f"{len(added)} appareil(s) ajouté(s) dans {home.save()}")
|
|
127
|
+
print(f" {st.paint('+', 'green', bold=True)} {st.code(f'{device.name:<18}')} {device.brand}")
|
|
128
|
+
print(st.ok(f"{len(added)} appareil(s) ajouté(s) dans {home.save()}"))
|
|
125
129
|
|
|
126
130
|
elif command == "list":
|
|
127
131
|
if remote:
|
|
@@ -131,21 +135,20 @@ def main(args):
|
|
|
131
135
|
else:
|
|
132
136
|
names, groups, scenes = list(home.devices), home.groups, list(home.scenes)
|
|
133
137
|
states = home.all().state() if home.devices else {}
|
|
134
|
-
print("Appareils
|
|
138
|
+
print(st.section("Appareils") if names else "Aucun appareil : lance " + st.code("capy domo scan"))
|
|
135
139
|
for name in names:
|
|
136
140
|
print(show_state(name, states.get(name, {"reachable": False})))
|
|
137
141
|
if groups:
|
|
138
|
-
print("
|
|
139
|
-
for name, members in groups.items()
|
|
140
|
-
print(f" {name:<18} {', '.join(members)}")
|
|
142
|
+
print("\n" + st.section("Groupes"))
|
|
143
|
+
print(st.table([(st.code(name), st.paint(", ".join(members), "dim")) for name, members in groups.items()]))
|
|
141
144
|
if scenes:
|
|
142
|
-
print("
|
|
145
|
+
print("\n" + st.section("Scènes") + " " + " ".join(st.code(s) for s in scenes))
|
|
143
146
|
|
|
144
147
|
elif command == "scene":
|
|
145
148
|
if not rest:
|
|
146
149
|
sys.exit("Usage : capy domo scene NOM")
|
|
147
150
|
home.scene(rest[0])
|
|
148
|
-
print(f"Scène {rest[0]!r} lancée")
|
|
151
|
+
print(st.ok(f"Scène {rest[0]!r} lancée"))
|
|
149
152
|
|
|
150
153
|
elif command == "serve" and not remote:
|
|
151
154
|
print("Maison pilotable à distance (actions domo.*), Ctrl+C pour arrêter")
|
|
@@ -167,7 +170,7 @@ def main(args):
|
|
|
167
170
|
target.toggle()
|
|
168
171
|
if changes:
|
|
169
172
|
target.set(**changes)
|
|
170
|
-
print(f"{command} :
|
|
173
|
+
print(st.ok(f"{command} : {' · '.join(rest)}"))
|
|
171
174
|
|
|
172
175
|
|
|
173
176
|
# Point d'entrée de capy domo
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: capyy
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.4
|
|
4
4
|
Summary: Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy
|
|
5
5
|
Author: Izac
|
|
6
6
|
License-Expression: MIT
|
|
@@ -111,6 +111,9 @@ capy network call home test --relay my-vps.example:3879 --key "$CAPY_KEY"
|
|
|
111
111
|
cy call hello nom=Izac # inside a project: uses capy.config.json
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
`capy cli` opens an interactive prompt (history, Tab completion, `cd` between projects). Output is colored in a
|
|
115
|
+
terminal and plain text / JSON in a pipe (set `NO_COLOR=1` to disable colors).
|
|
116
|
+
|
|
114
117
|
Parameters are `name=value`; numbers, `true`/`false` and lists are parsed (`age=14` sends the number 14).
|
|
115
118
|
The answer is printed as JSON; the exit code is 1 when the server answers `{"erreur": ...}`.
|
|
116
119
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "capyy"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.4"
|
|
8
8
|
description = "Easy, encrypted networking for Python: programs that talk (local, Wi-Fi, internet), home automation, and projects run with cy"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -403,8 +403,26 @@ class TestCommandLine(unittest.TestCase):
|
|
|
403
403
|
self.assertEqual(json.loads(result.stdout), {"message": "Bonjour Léa"})
|
|
404
404
|
self.assertIn("quel relais", self.run_capy("network", "call", "cli", "hello").stderr)
|
|
405
405
|
|
|
406
|
+
def test_interactive(self):
|
|
407
|
+
import subprocess
|
|
408
|
+
import sys
|
|
409
|
+
env = {**os.environ, "HOME": self.home.name, "PYTHONPATH": str(pathlib.Path(__file__).resolve().parent.parent)}
|
|
410
|
+
lines = f"call localhost:{self.server.port} hello name=Cli --no-tls\ncapy version\nfaute\ncd /\nexit\n"
|
|
411
|
+
result = subprocess.run([sys.executable, "-m", "capy", "cli"], input=lines, cwd=self.home.name, env=env,
|
|
412
|
+
capture_output=True, text=True, timeout=30)
|
|
413
|
+
self.assertIn('"message": "Bonjour Cli"', result.stdout) # La commande a marché
|
|
414
|
+
self.assertIn("capy ", result.stdout) # "capy version" (capy devant est accepté)
|
|
415
|
+
self.assertIn("pas un projet capy", result.stderr) # Une erreur n'arrête pas le mode interactif
|
|
416
|
+
self.assertEqual(result.returncode, 0)
|
|
417
|
+
self.assertFalse((pathlib.Path(self.home.name) / ".capy" / "history").exists()) # Pas un vrai terminal : pas d'historique
|
|
418
|
+
|
|
419
|
+
def test_plain_output_when_piped(self):
|
|
420
|
+
result = self.run_capy("help")
|
|
421
|
+
self.assertNotIn("\033[", result.stdout) # Pas de couleurs hors d'un terminal (| jq, fichiers…)
|
|
422
|
+
self.assertIn("capy cli", result.stdout)
|
|
423
|
+
|
|
406
424
|
def test_reserved_script_names(self):
|
|
407
|
-
for name in ("call", "wifi", "network"):
|
|
425
|
+
for name in ("call", "wifi", "network", "cli", "version"):
|
|
408
426
|
with self.assertRaisesRegex(ValueError, "nom d'une commande"):
|
|
409
427
|
path = pathlib.Path(self.home.name) / "capy.config.json"
|
|
410
428
|
path.write_text(json.dumps({"port": 1, "scripts": {name: "echo"}}))
|
capyy-0.5.3/capy/__main__.py
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
# Deux commandes, installées avec capy :
|
|
2
|
-
#
|
|
3
|
-
# cy : DANS un projet capy (un dossier avec capy.config.json, ou en dessous)
|
|
4
|
-
# cy -> montre le projet (nom, mode, adresse, fichiers, scripts…)
|
|
5
|
-
# cy NOM [args…] -> lance le script NOM de "scripts" (comme npm run)
|
|
6
|
-
# cy scripts -> liste les scripts
|
|
7
|
-
# cy server / cy client -> lance un fichier de "files"
|
|
8
|
-
# cy run -> lance le serveur en fond, puis le client, puis arrête le serveur
|
|
9
|
-
# cy call ACTION [nom=valeur…] -> appelle une action du serveur du projet (local, wifi ou network)
|
|
10
|
-
#
|
|
11
|
-
# capy : tout le reste (et tout ce que fait cy)
|
|
12
|
-
# capy new NOM [mode] -> crée un dossier NOM avec capy.config.json, server.py, client.py
|
|
13
|
-
# capy init [nom] [mode] -> crée juste capy.config.json ici
|
|
14
|
-
# capy domo … -> la domotique (capy domo help)
|
|
15
|
-
# capy relay PORT [CLÉ] -> lance un relais capy.network (sur le VPS)
|
|
16
|
-
# capy call HÔTE:PORT ACTION [nom=valeur…] -> appelle une action d'un serveur capy
|
|
17
|
-
# capy wifi scan / capy wifi call NOM ACTION … -> les serveurs du wifi
|
|
18
|
-
# capy network call NOM ACTION … [--relay R --key K] / capy network relay PORT [CLÉ]
|
|
19
|
-
# mode : local (par défaut), wifi ou network
|
|
20
|
-
# python3 -m capy = capy (si la commande n'est pas dans le PATH)
|
|
21
|
-
|
|
22
|
-
import difflib
|
|
23
|
-
import json
|
|
24
|
-
import socket
|
|
25
|
-
import subprocess
|
|
26
|
-
import sys
|
|
27
|
-
import threading
|
|
28
|
-
import time
|
|
29
|
-
|
|
30
|
-
from ._project import COMMANDS, FILE_NAME, MODES, find, init, new, project
|
|
31
|
-
|
|
32
|
-
USAGE = f"""Dans un projet capy ({FILE_NAME}) :
|
|
33
|
-
cy montre le projet
|
|
34
|
-
cy NOM [args…] lance le script NOM de "scripts" (comme npm run)
|
|
35
|
-
cy scripts liste les scripts
|
|
36
|
-
cy server / client lance un fichier de "files"
|
|
37
|
-
cy run serveur en fond + client, puis arrête le serveur
|
|
38
|
-
cy call ACTION [nom=valeur…] appelle une action du serveur du projet
|
|
39
|
-
Partout :
|
|
40
|
-
capy new NOM [mode] crée un projet complet (config, server.py, client.py)
|
|
41
|
-
capy init [nom] [mode] crée juste {FILE_NAME} ici
|
|
42
|
-
capy domo … la domotique (capy domo help)
|
|
43
|
-
capy relay PORT [CLÉ] lance un relais capy.network
|
|
44
|
-
capy call HÔTE:PORT ACTION [nom=valeur…] [--no-tls]
|
|
45
|
-
capy wifi scan les serveurs capy du wifi
|
|
46
|
-
capy wifi call NOM ACTION [nom=valeur…] [--no-tls]
|
|
47
|
-
capy network call NOM ACTION [nom=valeur…] [--relay HÔTE:PORT] [--key CLÉ] [--fingerprint …]
|
|
48
|
-
capy network relay PORT [CLÉ] (= capy relay)
|
|
49
|
-
Paramètres : nom=Izac age=14 actif=true liste=[1,2] (les nombres, true/false et listes sont compris)
|
|
50
|
-
modes : {", ".join(MODES)}"""
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# Le projet du dossier actuel (ou au-dessus), sinon on explique quoi faire
|
|
54
|
-
def _project():
|
|
55
|
-
path = find(".")
|
|
56
|
-
if path is None:
|
|
57
|
-
sys.exit(f"Pas de {FILE_NAME} ici ni au-dessus : ce dossier n'est pas un projet capy.\n"
|
|
58
|
-
f" Crée un projet : capy new mon-projet\n"
|
|
59
|
-
f" ou juste le fichier : capy init")
|
|
60
|
-
try:
|
|
61
|
-
return project(path)
|
|
62
|
-
except ValueError as e:
|
|
63
|
-
sys.exit(str(e))
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Sortir les options "--nom valeur" (et "--drapeau") d'une liste d'arguments -> (options, reste)
|
|
67
|
-
def _options(args, flags=(), values=()):
|
|
68
|
-
options, rest, i = {}, [], 0
|
|
69
|
-
while i < len(args):
|
|
70
|
-
word = args[i]
|
|
71
|
-
if word in flags:
|
|
72
|
-
options[word] = True
|
|
73
|
-
elif word in values:
|
|
74
|
-
if i + 1 >= len(args):
|
|
75
|
-
sys.exit(f"capy : il manque une valeur après {word}")
|
|
76
|
-
options[word] = args[i + 1]
|
|
77
|
-
i += 1
|
|
78
|
-
elif word.startswith("--"):
|
|
79
|
-
sys.exit(f"capy : option {word} inconnue (possibles : {', '.join((*flags, *values)) or 'aucune'})")
|
|
80
|
-
else:
|
|
81
|
-
rest.append(word)
|
|
82
|
-
i += 1
|
|
83
|
-
return options, rest
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# "nom=Izac" "age=14" -> {"nom": "Izac", "age": 14} : une valeur qui est du JSON (14, true, [1, 2]) est convertie
|
|
87
|
-
def _params(words):
|
|
88
|
-
params = {}
|
|
89
|
-
for word in words:
|
|
90
|
-
key, sep, value = word.partition("=")
|
|
91
|
-
if not sep or not key:
|
|
92
|
-
sys.exit(f"capy : paramètre {word!r} invalide, écris nom=valeur (ex : nom=Izac age=14)")
|
|
93
|
-
try:
|
|
94
|
-
params[key] = json.loads(value)
|
|
95
|
-
except ValueError:
|
|
96
|
-
params[key] = value
|
|
97
|
-
return params
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# Faire l'appel et afficher la réponse en JSON ; code de sortie 1 si erreur (pratique dans un script)
|
|
101
|
-
def _call(function):
|
|
102
|
-
import capy
|
|
103
|
-
try:
|
|
104
|
-
answer = function()
|
|
105
|
-
except (ConnectionError, ValueError, KeyError, capy.SecurityError) as e:
|
|
106
|
-
sys.exit(str(e).strip("'\""))
|
|
107
|
-
print(json.dumps(answer, indent=2, ensure_ascii=False))
|
|
108
|
-
if isinstance(answer, dict) and "erreur" in answer:
|
|
109
|
-
sys.exit(1)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
USAGE_CALL = "Usage : capy call HÔTE:PORT ACTION [nom=valeur…] [--no-tls] (ex : capy call localhost:4000 hello nom=Izac)"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# capy call localhost:4000 hello nom=Izac
|
|
116
|
-
def call_direct(args):
|
|
117
|
-
import capy
|
|
118
|
-
options, rest = _options(args, flags=("--no-tls",))
|
|
119
|
-
if len(rest) < 2:
|
|
120
|
-
sys.exit(USAGE_CALL)
|
|
121
|
-
host, _, port = rest[0].rpartition(":")
|
|
122
|
-
if not host or not port.isdigit():
|
|
123
|
-
sys.exit(f"capy : adresse {rest[0]!r} invalide, écris HÔTE:PORT (ex : localhost:4000)\n{USAGE_CALL}")
|
|
124
|
-
_call(lambda: capy.call(host, int(port), rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# capy wifi scan / capy wifi call maison hello nom=Izac
|
|
128
|
-
def wifi(args):
|
|
129
|
-
import capy.wifi
|
|
130
|
-
sub = args[0] if args else None
|
|
131
|
-
if sub == "scan":
|
|
132
|
-
options, _ = _options(args[1:], values=("--timeout",))
|
|
133
|
-
found = capy.wifi.scan(timeout=float(options.get("--timeout", 1)))
|
|
134
|
-
if not found:
|
|
135
|
-
sys.exit("Aucun serveur capy.wifi trouvé (même wifi ? wifi public qui isole les appareils ?)")
|
|
136
|
-
for name, (ip, port) in sorted(found.items()):
|
|
137
|
-
print(f" {name:<20} {ip}:{port}{' (cet appareil)' if ip == '127.0.0.1' else ''}")
|
|
138
|
-
elif sub == "call":
|
|
139
|
-
options, rest = _options(args[1:], flags=("--no-tls",))
|
|
140
|
-
if len(rest) < 2:
|
|
141
|
-
sys.exit("Usage : capy wifi call NOM ACTION [nom=valeur…] [--no-tls] (ex : capy wifi call maison hello)")
|
|
142
|
-
_call(lambda: capy.wifi.call(rest[0], rest[1], tls="--no-tls" not in options, **_params(rest[2:])))
|
|
143
|
-
else:
|
|
144
|
-
sys.exit("Usage : capy wifi scan [--timeout S] ou capy wifi call NOM ACTION [nom=valeur…] [--no-tls]")
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
# capy network call maison hello --relay mon-vps.fr:3879 --key … / capy network relay 3879 CLÉ
|
|
148
|
-
# Relais et clé : options, sinon le capy.config.json du dossier (mode network), sinon $CAPY_RELAY / $CAPY_KEY
|
|
149
|
-
def network(args):
|
|
150
|
-
import os
|
|
151
|
-
|
|
152
|
-
import capy.network
|
|
153
|
-
sub = args[0] if args else None
|
|
154
|
-
if sub == "relay" or (sub or "").isdigit(): # capy network relay 3879 CLÉ (ou l'ancien capy network 3879 CLÉ)
|
|
155
|
-
capy.network.cli(args[1:] if sub == "relay" else args)
|
|
156
|
-
elif sub == "call":
|
|
157
|
-
options, rest = _options(args[1:], values=("--relay", "--key", "--fingerprint"))
|
|
158
|
-
if len(rest) < 2:
|
|
159
|
-
sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay HÔTE:PORT] [--key CLÉ] [--fingerprint …]")
|
|
160
|
-
relay, key, fingerprint = options.get("--relay"), options.get("--key"), options.get("--fingerprint")
|
|
161
|
-
path = find(".")
|
|
162
|
-
if path and not relay:
|
|
163
|
-
try:
|
|
164
|
-
app = project(path)
|
|
165
|
-
except ValueError:
|
|
166
|
-
app = None
|
|
167
|
-
if app and app.mode == "network":
|
|
168
|
-
relay, key, fingerprint = app.relay, key or app.key, fingerprint or app.fingerprint
|
|
169
|
-
relay, key = relay or os.environ.get("CAPY_RELAY"), key or os.environ.get("CAPY_KEY")
|
|
170
|
-
if not relay:
|
|
171
|
-
sys.exit("capy : quel relais ? Ajoute --relay mon-vps.fr:3879 (ou un capy.config.json en mode network, ou $CAPY_RELAY)")
|
|
172
|
-
capy.network.use(relay, key=key, fingerprint=fingerprint)
|
|
173
|
-
_call(lambda: capy.network.call(rest[0], rest[1], **_params(rest[2:])))
|
|
174
|
-
else:
|
|
175
|
-
sys.exit("Usage : capy network call NOM ACTION [nom=valeur…] [--relay …] [--key …] ou capy network relay PORT [CLÉ]")
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# Lancer un fichier du projet avec le même Python, depuis le dossier du projet
|
|
179
|
-
def _start(app, name, args=(), **options):
|
|
180
|
-
try:
|
|
181
|
-
path = app.file(name)
|
|
182
|
-
except KeyError as e:
|
|
183
|
-
sys.exit(e.args[0])
|
|
184
|
-
if not path.is_file():
|
|
185
|
-
sys.exit(f"capy : le fichier {name!r} ({path}) n'existe pas")
|
|
186
|
-
return subprocess.Popen([sys.executable, "-u", str(path), *args], cwd=app.folder, **options)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
# Attendre que le serveur réponde (10 s max) ; False s'il s'est arrêté avant
|
|
190
|
-
def _wait_ready(app, process):
|
|
191
|
-
end = time.monotonic() + 10
|
|
192
|
-
while time.monotonic() < end:
|
|
193
|
-
if process.poll() is not None:
|
|
194
|
-
return False
|
|
195
|
-
try:
|
|
196
|
-
if app.mode == "local" and app.port:
|
|
197
|
-
socket.create_connection((app.host, app.port), timeout=0.3).close()
|
|
198
|
-
return True
|
|
199
|
-
if app.mode == "wifi":
|
|
200
|
-
import capy.wifi
|
|
201
|
-
capy.wifi._found.pop(app.name, None)
|
|
202
|
-
capy.wifi.find(app.name, timeout=0.3)
|
|
203
|
-
return True
|
|
204
|
-
if app.mode == "network" or not app.port:
|
|
205
|
-
time.sleep(2) # Pas de moyen simple de vérifier : on laisse 2 s au serveur
|
|
206
|
-
return process.poll() is None
|
|
207
|
-
except (OSError, ConnectionError):
|
|
208
|
-
time.sleep(0.2)
|
|
209
|
-
return process.poll() is None
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Recopier la sortie du serveur, avec "server │" devant chaque ligne
|
|
213
|
-
def _relay_output(process):
|
|
214
|
-
for line in process.stdout:
|
|
215
|
-
print(f"\033[2mserver │\033[0m {line}", end="", flush=True)
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
def run(app, args):
|
|
219
|
-
server = _start(app, "server", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
220
|
-
threading.Thread(target=_relay_output, args=(server,), daemon=True).start()
|
|
221
|
-
try:
|
|
222
|
-
if not _wait_ready(app, server):
|
|
223
|
-
time.sleep(0.2) # Laisser s'afficher l'erreur du serveur
|
|
224
|
-
sys.exit(f"capy : le serveur s'est arrêté (ou ne répond pas après 10 s)")
|
|
225
|
-
client = _start(app, "client", args)
|
|
226
|
-
code = client.wait()
|
|
227
|
-
except KeyboardInterrupt:
|
|
228
|
-
code = 130
|
|
229
|
-
finally:
|
|
230
|
-
server.terminate()
|
|
231
|
-
try:
|
|
232
|
-
server.wait(timeout=3)
|
|
233
|
-
except subprocess.TimeoutExpired:
|
|
234
|
-
server.kill()
|
|
235
|
-
sys.exit(code)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def main(args):
|
|
239
|
-
command = args[0] if args else None
|
|
240
|
-
if command in ("-h", "--help", "help"):
|
|
241
|
-
print(USAGE)
|
|
242
|
-
return
|
|
243
|
-
if command == "domo": # capy domo salon on
|
|
244
|
-
from .domo.__main__ import cli as domo_cli
|
|
245
|
-
domo_cli(args[1:])
|
|
246
|
-
return
|
|
247
|
-
if command == "relay": # capy relay 3879 CLÉ
|
|
248
|
-
from .network import cli as relay_cli
|
|
249
|
-
relay_cli(args[1:])
|
|
250
|
-
return
|
|
251
|
-
if command == "network":
|
|
252
|
-
network(args[1:])
|
|
253
|
-
return
|
|
254
|
-
if command == "wifi":
|
|
255
|
-
wifi(args[1:])
|
|
256
|
-
return
|
|
257
|
-
if command == "call" and len(args) > 1 and ":" in args[1]: # capy call localhost:4000 hello
|
|
258
|
-
call_direct(args[1:])
|
|
259
|
-
return
|
|
260
|
-
if command in ("init", "new"):
|
|
261
|
-
rest = args[1:]
|
|
262
|
-
mode = next((a for a in rest if a in MODES), "local")
|
|
263
|
-
name = next((a for a in rest if a not in MODES), None)
|
|
264
|
-
if command == "new" and not name:
|
|
265
|
-
sys.exit("Usage : capy new NOM [mode]")
|
|
266
|
-
try:
|
|
267
|
-
path = new(name, mode) / FILE_NAME if command == "new" else init(name=name, mode=mode)
|
|
268
|
-
except (FileExistsError, ValueError) as e:
|
|
269
|
-
sys.exit(str(e))
|
|
270
|
-
print(f"Créé : {path.parent if command == 'new' else path}")
|
|
271
|
-
print(project(path).describe())
|
|
272
|
-
if command == "new":
|
|
273
|
-
print(f"\nPour l'essayer :\n cd {name}\n cy run")
|
|
274
|
-
if mode == "network":
|
|
275
|
-
print("Pense à : export CAPY_KEY='la clé du relais' (dans ~/.zshrc), et à mettre l'adresse de ton relais")
|
|
276
|
-
return
|
|
277
|
-
app = _project()
|
|
278
|
-
if command is None:
|
|
279
|
-
print(app.describe())
|
|
280
|
-
elif command == "run":
|
|
281
|
-
run(app, args[1:])
|
|
282
|
-
elif command == "call": # cy call hello nom=Izac : le serveur du projet, quel que soit son mode
|
|
283
|
-
if len(args) < 2:
|
|
284
|
-
sys.exit("Usage : cy call ACTION [nom=valeur…] (ex : cy call hello nom=Izac)")
|
|
285
|
-
_call(lambda: app.call(args[1], **_params(args[2:])))
|
|
286
|
-
elif command == "scripts":
|
|
287
|
-
if not app.scripts:
|
|
288
|
-
print('Pas de "scripts" dans ' + FILE_NAME + ', ex : "scripts": {"test": "python3 -m unittest"}')
|
|
289
|
-
for name, line in app.scripts.items():
|
|
290
|
-
print(f" {name:<12} {line}")
|
|
291
|
-
elif command in app.scripts:
|
|
292
|
-
print(f"\033[2m> {app.scripts[command]}\033[0m", flush=True) # Comme npm : on montre la commande lancée
|
|
293
|
-
try:
|
|
294
|
-
sys.exit(app.run_script(command, args[1:]))
|
|
295
|
-
except KeyboardInterrupt:
|
|
296
|
-
sys.exit(130)
|
|
297
|
-
elif command in app.files:
|
|
298
|
-
try:
|
|
299
|
-
sys.exit(_start(app, command, args[1:]).wait())
|
|
300
|
-
except KeyboardInterrupt:
|
|
301
|
-
sys.exit(130)
|
|
302
|
-
else:
|
|
303
|
-
names = list(app.scripts) + list(app.files) + list(COMMANDS)
|
|
304
|
-
close = difflib.get_close_matches(command, names, n=1)
|
|
305
|
-
if close:
|
|
306
|
-
sys.exit(f"capy : {command!r} inconnu, tu voulais dire {close[0]!r} ? (cy {close[0]})")
|
|
307
|
-
sys.exit(f"capy : commande, script ou fichier {command!r} inconnu (possibles : {', '.join(names)})\n\n{USAGE}")
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
# La commande cy : seulement les commandes d'un projet
|
|
311
|
-
def cli():
|
|
312
|
-
args = sys.argv[1:]
|
|
313
|
-
if args[:1] in (["new"], ["init"], ["domo"], ["relay"], ["network"], ["wifi"]):
|
|
314
|
-
sys.exit(f"cy sert dans un projet capy ; pour ça, c'est la commande capy : capy {' '.join(args)}")
|
|
315
|
-
main(args)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
# La commande capy (et python3 -m capy) : tout
|
|
319
|
-
def capy_cli():
|
|
320
|
-
main(sys.argv[1:])
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
if __name__ == "__main__":
|
|
324
|
-
capy_cli()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|