capyy 0.3.2__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capyy
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet
5
5
  Author: Izac
6
6
  License-Expression: MIT
@@ -73,6 +73,23 @@ capy.to_int("42"), capy.to_bool("yes") # 42, True
73
73
  `to_str`, `to_int`, `to_float`, `to_bool`, `to_list` and `to_dict` raise a `ValueError` that says exactly
74
74
  what could not be converted.
75
75
 
76
+ ## Project file: capy.config.json
77
+
78
+ Describe the project once (`python3 -m capy init my-game` creates it), then never repeat ports or hosts:
79
+
80
+ ```json
81
+ {"name": "my-game", "mode": "local", "host": "localhost", "port": 4000, "tls": true}
82
+ ```
83
+
84
+ ```python
85
+ app = capy.project() # finds capy.config.json next to the script, or in a parent folder
86
+ server = app.server() # server side
87
+ app.call("hello", nom="Izac") # client side
88
+ ```
89
+
90
+ `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
91
+ from environment variables, e.g. `"key": "$CAPY_KEY"`.
92
+
76
93
  ## Encryption (TLS)
77
94
 
78
95
  Everything is encrypted by default, with nothing to configure.
@@ -51,6 +51,23 @@ capy.to_int("42"), capy.to_bool("yes") # 42, True
51
51
  `to_str`, `to_int`, `to_float`, `to_bool`, `to_list` and `to_dict` raise a `ValueError` that says exactly
52
52
  what could not be converted.
53
53
 
54
+ ## Project file: capy.config.json
55
+
56
+ Describe the project once (`python3 -m capy init my-game` creates it), then never repeat ports or hosts:
57
+
58
+ ```json
59
+ {"name": "my-game", "mode": "local", "host": "localhost", "port": 4000, "tls": true}
60
+ ```
61
+
62
+ ```python
63
+ app = capy.project() # finds capy.config.json next to the script, or in a parent folder
64
+ server = app.server() # server side
65
+ app.call("hello", nom="Izac") # client side
66
+ ```
67
+
68
+ `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
69
+ from environment variables, e.g. `"key": "$CAPY_KEY"`.
70
+
54
71
  ## Encryption (TLS)
55
72
 
56
73
  Everything is encrypted by default, with nothing to configure.
@@ -6,6 +6,7 @@ import threading
6
6
  from typing import Any
7
7
 
8
8
  from . import _tls
9
+ from ._project import Project, project # noqa: F401 (capy.project() : lire capy.config.json)
9
10
  from ._tls import SecurityError # noqa: F401 (capy.SecurityError)
10
11
 
11
12
 
@@ -51,7 +52,7 @@ class Server:
51
52
  line = conn.makefile(encoding="utf-8").readline()
52
53
  if not line.strip(): # Connexion ouverte puis fermée sans rien demander
53
54
  return
54
- conn.sendall((jsonlib.dumps(self.handle(line), ensure_ascii=False) + "\n").encode())
55
+ conn.sendall((_dumps(self.handle(line)) + "\n").encode())
55
56
  except (OSError, ValueError):
56
57
  return
57
58
  print(f"{addr[0]} → {_hide(line)}")
@@ -76,6 +77,15 @@ class Server:
76
77
 
77
78
 
78
79
  # Renvoyer du JSON écrit en texte depuis une action
80
+ # Réponse d'une action -> texte JSON (une réponse impossible à mettre en JSON devient une erreur claire)
81
+ # Ex : return {1, 2} (un set) -> {"erreur": "réponse pas en JSON : Object of type set is not JSON serializable"}
82
+ def _dumps(response):
83
+ try:
84
+ return jsonlib.dumps(response, ensure_ascii=False)
85
+ except (TypeError, ValueError) as e:
86
+ return jsonlib.dumps({"erreur": f"réponse pas en JSON : {e}"}, ensure_ascii=False)
87
+
88
+
79
89
  # Paramètres jamais affichés dans le journal du serveur (mots de passe, tokens…)
80
90
  SECRETS = ("password", "passwd", "pwd", "mdp", "motdepasse", "secret", "token")
81
91
 
@@ -284,6 +294,8 @@ def _call(host, port, action, params, tls, label, who="capy"):
284
294
  error = e
285
295
  except (OSError, ValueError) as e:
286
296
  error = e
297
+ if connected and isinstance(error, socket.timeout): # Connecté, mais l'action met trop longtemps
298
+ raise ConnectionError(f"{who} : le serveur {host}:{port} n'a pas répondu à temps (3 s) : l'action est sans doute trop lente") from None
287
299
  if connected: # Il a décroché puis coupé : souvent un serveur chiffré appelé en clair
288
300
  raise ConnectionError(f"{who} : le serveur {host}:{port} a coupé la connexion sans répondre{hint}") from None
289
301
  raise ConnectionError(f"{who} : le serveur {host}:{port} ne répond pas ({type(error).__name__})") from None
@@ -0,0 +1,36 @@
1
+ # python3 -m capy : petites commandes pour un projet capy
2
+ #
3
+ # python3 -m capy init [nom] [local|wifi|network] -> crée capy.config.json ici
4
+ # python3 -m capy -> montre le projet trouvé (capy.config.json)
5
+
6
+ import sys
7
+
8
+ from ._project import FILE_NAME, MODES, find, init, project
9
+
10
+
11
+ def main(args):
12
+ if args[:1] == ["init"]:
13
+ rest = args[1:]
14
+ mode = next((a for a in rest if a in MODES), "local")
15
+ name = next((a for a in rest if a not in MODES), None)
16
+ try:
17
+ path = init(name=name, mode=mode)
18
+ except (FileExistsError, ValueError) as e:
19
+ sys.exit(str(e))
20
+ print(f"Créé : {path}")
21
+ print(path.read_text(encoding="utf-8"), end="")
22
+ if mode == "network":
23
+ print("Pense à : export CAPY_KEY='la clé du relais' (dans ~/.zshrc), et à mettre l'adresse de ton relais")
24
+ return
25
+ if args:
26
+ sys.exit(f"Usage : python3 -m capy init [nom] [{'|'.join(MODES)}] ou python3 -m capy")
27
+ path = find(".")
28
+ if path is None:
29
+ sys.exit(f"Pas de {FILE_NAME} ici ni au-dessus. Crée-le avec : python3 -m capy init")
30
+ try:
31
+ print(project(path), "\n ", path)
32
+ except ValueError as e:
33
+ sys.exit(str(e))
34
+
35
+
36
+ main(sys.argv[1:])
@@ -0,0 +1,188 @@
1
+ # capy.project() : décrire un projet une fois dans capy.config.json, puis ne plus répéter port, IP, relais...
2
+ #
3
+ # capy.config.json {"name": "maison", "mode": "local", "host": "localhost", "port": 4000}
4
+ # serveur.py app = capy.project(); server = app.server(); ...; server.run()
5
+ # client.py app = capy.project(); app.call("hello", nom="Izac")
6
+ #
7
+ # mode "local" -> capy.Server(port) / capy.call(host, port, ...)
8
+ # mode "wifi" -> capy.wifi.Server(name) / capy.wifi.call(name, ...)
9
+ # mode "network" -> capy.network.Server(name) / capy.network.call(name, ...) via "relay"
10
+ # Un texte qui commence par $ est lu dans les variables d'environnement : "key": "$CAPY_KEY"
11
+
12
+ import difflib
13
+ import json as jsonlib
14
+ import os
15
+ import pathlib as pth
16
+ import sys
17
+ from typing import Any
18
+
19
+ FILE_NAME = "capy.config.json"
20
+ MODES = ("local", "wifi", "network")
21
+
22
+ # Ce qu'on peut mettre dans le fichier : clé -> (types acceptés, explication)
23
+ FIELDS = {
24
+ "name": ((str,), "nom du projet (le nom du serveur en wifi et en network)"),
25
+ "mode": ((str,), "local, wifi ou network"),
26
+ "host": ((str,), "adresse du serveur pour le client, en local (ex : localhost, 192.168.1.42)"),
27
+ "port": ((int,), "port du serveur (0 = un port libre au hasard)"),
28
+ "tls": ((bool,), "chiffrement, true par défaut"),
29
+ "relay": ((str,), "relais capy.network, ex : mon-vps.fr:3879"),
30
+ "key": ((str,), "clé du relais (mieux : \"$CAPY_KEY\", lue dans les variables d'environnement)"),
31
+ "fingerprint": ((str,), "empreinte du relais, affichée à son démarrage"),
32
+ "description": ((str,), "une phrase pour toi, capy ne s'en sert pas"),
33
+ }
34
+
35
+ EXAMPLE = {"name": "mon-projet", "mode": "local", "host": "localhost", "port": 4000, "tls": True}
36
+
37
+
38
+ # Dossier du script lancé (comme simplesuit.json) ; sans script (python3 -c, REPL) -> dossier actuel
39
+ def _main_dir():
40
+ file = getattr(sys.modules.get("__main__"), "__file__", None)
41
+ return pth.Path(file).resolve().parent if file else pth.Path.cwd()
42
+
43
+
44
+ # Chercher capy.config.json dans le dossier du script, puis dans les dossiers au-dessus (comme git)
45
+ def find(start=None):
46
+ folder = pth.Path(start or _main_dir()).resolve()
47
+ for place in (folder, *folder.parents):
48
+ if (place / FILE_NAME).is_file():
49
+ return place / FILE_NAME
50
+ return None
51
+
52
+
53
+ # Un projet capy, lu depuis capy.config.json
54
+ # Ex : app = capy.project() puis app.server() côté serveur, app.call("hello") côté client
55
+ class Project:
56
+ def __init__(self, path):
57
+ self.path = pth.Path(path)
58
+ try:
59
+ data = jsonlib.loads(self.path.read_text(encoding="utf-8"))
60
+ except FileNotFoundError:
61
+ raise FileNotFoundError(f"capy.project : pas de fichier {self.path}") from None
62
+ except ValueError as e:
63
+ raise ValueError(f"capy.project : {self.path.name} n'est pas du JSON valide ({e})") from None
64
+ if not isinstance(data, dict):
65
+ raise ValueError(f"capy.project : {self.path.name} doit contenir un objet {{...}}")
66
+ self.config = _check(data, self.path.name)
67
+ get = self.config.get
68
+ self.name = get("name") or self.path.parent.name
69
+ self.mode = get("mode") or ("network" if get("relay") else "local")
70
+ self.host = get("host", "localhost")
71
+ self.port = get("port")
72
+ self.tls = get("tls", True)
73
+ self.relay, self.key, self.fingerprint = get("relay"), get("key"), get("fingerprint")
74
+ self._require()
75
+
76
+ def __repr__(self):
77
+ where = {"local": f"local, {self.host}:{self.port}", "wifi": "sur le wifi", "network": f"via le relais {self.relay}"}[self.mode]
78
+ return f"<capy.Project {self.name!r} ({where}, {'chiffré' if self.tls else 'en clair'})>"
79
+
80
+ # Vérifier qu'il ne manque rien pour ce mode
81
+ def _require(self):
82
+ if self.mode not in MODES:
83
+ raise ValueError(f"capy.project : mode {self.mode!r} inconnu dans {self.path.name}, choisis parmi : {', '.join(MODES)}")
84
+ if self.mode == "local" and self.port is None:
85
+ raise ValueError(f'capy.project : il manque "port" dans {self.path.name} (ex : "port": 4000)')
86
+ if self.mode == "network" and not self.relay:
87
+ raise ValueError(f'capy.project : il manque "relay" dans {self.path.name} (ex : "relay": "mon-vps.fr:3879")')
88
+ if self.mode == "network" and not self.tls:
89
+ raise ValueError("capy.project : capy.network est toujours chiffré, enlève \"tls\": false")
90
+
91
+ # Le serveur de ce projet, déjà réglé (port, nom, chiffrement, relais)
92
+ # Ex : server = app.server() ; @server.action("hello") ... ; server.run()
93
+ def server(self):
94
+ import capy
95
+ if self.mode == "local":
96
+ return capy.Server(self.port, tls=self.tls)
97
+ if self.mode == "wifi":
98
+ import capy.wifi
99
+ return capy.wifi.Server(self.name, port=self.port or 0, tls=self.tls)
100
+ self._use_relay()
101
+ import capy.network
102
+ return capy.network.Server(self.name)
103
+
104
+ # Appeler une action du serveur de ce projet (sans redonner l'IP, le port ni le relais)
105
+ # Ex : app.call("hello", nom="Izac") -> {"message": "Bonjour Izac !"}
106
+ def call(self, action, /, **params) -> Any:
107
+ import capy
108
+ if self.mode == "local":
109
+ return capy.call(self.host, self.port, action, tls=self.tls, **params)
110
+ if self.mode == "wifi":
111
+ import capy.wifi
112
+ return capy.wifi.call(self.name, action, tls=self.tls, **params)
113
+ self._use_relay()
114
+ import capy.network
115
+ return capy.network.call(self.name, action, **params)
116
+
117
+ # Régler capy.network sur le relais du projet (seulement s'il a changé : use() ferme les connexions ouvertes)
118
+ def _use_relay(self):
119
+ import capy.network
120
+ wanted = {"relay": self.relay, "key": self.key, "fingerprint": self.fingerprint}
121
+ if capy.network._config != wanted:
122
+ capy.network.use(self.relay, key=self.key, fingerprint=self.fingerprint)
123
+
124
+
125
+ # Vérifier le contenu du fichier : clés connues, bons types, variables d'environnement
126
+ def _check(data, file):
127
+ result = {}
128
+ for key, value in data.items():
129
+ if key not in FIELDS:
130
+ close = difflib.get_close_matches(key, FIELDS, n=1)
131
+ hint = f" (tu voulais dire {close[0]!r} ?)" if close else f" (clés possibles : {', '.join(FIELDS)})"
132
+ raise ValueError(f"capy.project : clé {key!r} inconnue dans {file}{hint}")
133
+ types, help_text = FIELDS[key]
134
+ if isinstance(value, str) and value.startswith("$") and len(value) > 1:
135
+ env = value[1:]
136
+ if env not in os.environ:
137
+ raise ValueError(f"capy.project : {key!r} vaut {value!r} mais la variable d'environnement {env} n'existe pas"
138
+ f" (ajoute export {env}='...' dans ~/.zshrc)")
139
+ value = os.environ[env]
140
+ if int in types and value.isdigit():
141
+ value = int(value)
142
+ if value is None:
143
+ continue
144
+ if not isinstance(value, types) or (bool not in types and isinstance(value, bool)):
145
+ raise ValueError(f"capy.project : {key!r} doit être {_type_name(types)} dans {file} : {help_text}")
146
+ result[key] = value
147
+ return result
148
+
149
+
150
+ def _type_name(types):
151
+ return {str: "un texte", int: "un nombre", bool: "true ou false"}[types[0]]
152
+
153
+
154
+ # Lire le capy.config.json du projet -> Project
155
+ # Sans chemin : cherché à côté du script lancé, puis dans les dossiers au-dessus
156
+ # Ex : app = capy.project() / capy.project("autre.json") / capy.project("~/projets/jeu")
157
+ def project(path=None) -> Project:
158
+ if path is None:
159
+ found = find()
160
+ if found is None:
161
+ raise FileNotFoundError(
162
+ f"capy.project : aucun {FILE_NAME} trouvé dans {_main_dir()} ni au-dessus.\n"
163
+ f" Crée-le avec : python3 -m capy init\n"
164
+ f" ou écris-le toi-même, par exemple : {jsonlib.dumps(EXAMPLE, ensure_ascii=False)}"
165
+ )
166
+ return Project(found)
167
+ path = pth.Path(path).expanduser()
168
+ if not path.is_absolute():
169
+ path = _main_dir() / path
170
+ return Project(path / FILE_NAME if path.is_dir() else path)
171
+
172
+
173
+ # Écrire un capy.config.json de départ (python3 -m capy init [nom] [mode])
174
+ def init(folder=None, name=None, mode="local"):
175
+ if mode not in MODES:
176
+ raise ValueError(f"capy init : mode {mode!r} inconnu, choisis parmi : {', '.join(MODES)}")
177
+ folder = pth.Path(folder or pth.Path.cwd())
178
+ path = folder / FILE_NAME
179
+ if path.exists():
180
+ raise FileExistsError(f"capy init : {path} existe déjà (supprime-le d'abord pour en refaire un)")
181
+ data = {"name": name or folder.resolve().name, "mode": mode}
182
+ if mode == "local":
183
+ data.update(host="localhost", port=4000)
184
+ elif mode == "network":
185
+ data.update(relay="mon-vps.fr:3879", key="$CAPY_KEY")
186
+ data["tls"] = True
187
+ path.write_text(jsonlib.dumps(data, indent=4, ensure_ascii=False) + "\n", encoding="utf-8")
188
+ return path
@@ -296,7 +296,7 @@ class Relay:
296
296
  except ConnectionError:
297
297
  return
298
298
  if reply["capy"] == "ok":
299
- _log(f" {name} ← {jsonlib.dumps(request, ensure_ascii=False)}")
299
+ _log(f" {name} ← {capy._hide(request)}")
300
300
 
301
301
  def run(self):
302
302
  self.context, fingerprint = _relay_context()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capyy
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet
5
5
  Author: Izac
6
6
  License-Expression: MIT
@@ -73,6 +73,23 @@ capy.to_int("42"), capy.to_bool("yes") # 42, True
73
73
  `to_str`, `to_int`, `to_float`, `to_bool`, `to_list` and `to_dict` raise a `ValueError` that says exactly
74
74
  what could not be converted.
75
75
 
76
+ ## Project file: capy.config.json
77
+
78
+ Describe the project once (`python3 -m capy init my-game` creates it), then never repeat ports or hosts:
79
+
80
+ ```json
81
+ {"name": "my-game", "mode": "local", "host": "localhost", "port": 4000, "tls": true}
82
+ ```
83
+
84
+ ```python
85
+ app = capy.project() # finds capy.config.json next to the script, or in a parent folder
86
+ server = app.server() # server side
87
+ app.call("hello", nom="Izac") # client side
88
+ ```
89
+
90
+ `mode` can be `local`, `wifi` or `network` (with `relay`, `key`, `fingerprint`). Strings starting with `$` are read
91
+ from environment variables, e.g. `"key": "$CAPY_KEY"`.
92
+
76
93
  ## Encryption (TLS)
77
94
 
78
95
  Everything is encrypted by default, with nothing to configure.
@@ -2,6 +2,8 @@ LICENSE
2
2
  README.md
3
3
  pyproject.toml
4
4
  capy/__init__.py
5
+ capy/__main__.py
6
+ capy/_project.py
5
7
  capy/_tls.py
6
8
  capy/network.py
7
9
  capy/wifi.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "capyy"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "Tiny encrypted JSON-over-TCP APIs: call a function in another program, on your Wi-Fi or across the internet"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,5 +1,7 @@
1
1
  # Tests de capy : python3 -m unittest discover -s tests
2
2
 
3
+ import json
4
+ import os
3
5
  import pathlib
4
6
  import socket
5
7
  import tempfile
@@ -48,6 +50,15 @@ def demo_server(server):
48
50
  def text():
49
51
  return capy.json('{"text": "Hello"}')
50
52
 
53
+ @server.action("not_json")
54
+ def not_json():
55
+ return {1, 2} # Un set : impossible en JSON
56
+
57
+ @server.action("slow")
58
+ def slow():
59
+ time.sleep(3.5)
60
+ return {"ok": True}
61
+
51
62
  return server
52
63
 
53
64
 
@@ -127,6 +138,14 @@ class TestCall(unittest.TestCase):
127
138
  with self.assertRaises(ConnectionError):
128
139
  capy.call("localhost", free_port(), "hello")
129
140
 
141
+ def test_not_json_reply(self):
142
+ reply = capy.call("localhost", self.tls.port, "not_json")
143
+ self.assertIn("réponse pas en JSON", reply["erreur"])
144
+
145
+ def test_too_slow(self):
146
+ with self.assertRaisesRegex(ConnectionError, "pas répondu à temps"):
147
+ capy.call("localhost", self.plain.port, "slow", tls=False)
148
+
130
149
  def test_bad_arguments(self):
131
150
  with self.assertRaises(TypeError):
132
151
  capy.call(4000, "hello", "localhost")
@@ -203,5 +222,82 @@ class TestNetwork(unittest.TestCase):
203
222
  capy.network.call("maison", "hello")
204
223
 
205
224
 
225
+ class TestProject(unittest.TestCase):
226
+ def setUp(self):
227
+ self.tmp = tempfile.TemporaryDirectory()
228
+ self.dir = pathlib.Path(self.tmp.name)
229
+
230
+ def tearDown(self):
231
+ self.tmp.cleanup()
232
+
233
+ def write(self, data, folder=None):
234
+ path = (folder or self.dir) / "capy.config.json"
235
+ path.write_text(json.dumps(data))
236
+ return path
237
+
238
+ def test_local_server_and_call(self):
239
+ port = free_port()
240
+ self.write({"name": "demo", "mode": "local", "host": "localhost", "port": port})
241
+ app = capy.project(self.dir)
242
+ self.assertEqual((app.name, app.mode, app.port, app.tls), ("demo", "local", port, True))
243
+ start(demo_server(app.server()))
244
+ self.assertEqual(app.call("hello", name="Zoe"), {"message": "Bonjour Zoe"})
245
+
246
+ def test_find_in_parent_folder(self):
247
+ self.write({"port": 4000})
248
+ sub = self.dir / "client" / "src"
249
+ sub.mkdir(parents=True)
250
+ self.assertEqual(capy._project.find(sub), (self.dir / "capy.config.json").resolve())
251
+ app = capy.project(self.dir / "capy.config.json")
252
+ self.assertEqual((app.name, app.mode, app.host), (self.dir.name, "local", "localhost")) # Valeurs par défaut
253
+
254
+ def test_env_variables(self):
255
+ os.environ["CAPY_TEST_KEY"], os.environ["CAPY_TEST_PORT"] = "s3cret", "4100"
256
+ self.write({"relay": "mon-vps.fr:3879", "key": "$CAPY_TEST_KEY", "port": "$CAPY_TEST_PORT"})
257
+ app = capy.project(self.dir)
258
+ self.assertEqual((app.mode, app.key, app.port), ("network", "s3cret", 4100)) # relay -> mode network
259
+ self.write({"relay": "mon-vps.fr:3879", "key": "$CAPY_PAS_LA"})
260
+ with self.assertRaisesRegex(ValueError, "CAPY_PAS_LA n'existe pas"):
261
+ capy.project(self.dir)
262
+
263
+ def test_errors(self):
264
+ with self.assertRaisesRegex(FileNotFoundError, "pas de fichier"):
265
+ capy.project(self.dir / "absent.json")
266
+ for data, message in (
267
+ ({"prot": 4000}, "tu voulais dire 'port'"),
268
+ ({"port": "4000"}, "doit être un nombre"),
269
+ ({"port": True}, "doit être un nombre"),
270
+ ({"mode": "local"}, 'il manque "port"'),
271
+ ({"mode": "bluetooth"}, "mode 'bluetooth' inconnu"),
272
+ ({"mode": "network"}, 'il manque "relay"'),
273
+ ({"tls": "oui", "port": 1}, "true ou false"),
274
+ ):
275
+ self.write(data)
276
+ with self.assertRaisesRegex(ValueError, message):
277
+ capy.project(self.dir)
278
+ (self.dir / "capy.config.json").write_text("{pas du json")
279
+ with self.assertRaisesRegex(ValueError, "pas du JSON valide"):
280
+ capy.project(self.dir)
281
+
282
+ def test_wifi_and_network_servers(self):
283
+ self.write({"name": "salon", "mode": "wifi", "tls": False})
284
+ server = capy.project(self.dir).server()
285
+ self.assertIsInstance(server, capy.wifi.Server)
286
+ self.assertEqual((server.name, server.port, server.tls), ("salon", 0, False))
287
+ self.write({"name": "garage", "relay": "127.0.0.1:1", "key": "k"})
288
+ self.assertIsInstance(capy.project(self.dir).server(), capy.network.Server)
289
+ self.assertEqual(capy.network._config["relay"], "127.0.0.1:1")
290
+
291
+ def test_init(self):
292
+ path = capy._project.init(self.dir, name="jeu")
293
+ self.assertEqual(json.loads(path.read_text())["name"], "jeu")
294
+ self.assertEqual(capy.project(path).port, 4000)
295
+ with self.assertRaises(FileExistsError):
296
+ capy._project.init(self.dir)
297
+ (self.dir / "net").mkdir()
298
+ path = capy._project.init(self.dir / "net", mode="network")
299
+ self.assertEqual(json.loads(path.read_text())["key"], "$CAPY_KEY")
300
+
301
+
206
302
  if __name__ == "__main__":
207
303
  unittest.main()
File without changes
File without changes
File without changes
File without changes