cubething_psync 0.2.1.dev2__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.
Files changed (25) hide show
  1. {cubething_psync-0.2.1.dev2/src/cubething_psync.egg-info → cubething_psync-0.4.0}/PKG-INFO +1 -1
  2. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/pyproject.toml +18 -1
  3. cubething_psync-0.4.0/src/client/args.py +207 -0
  4. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/client/main.py +71 -64
  5. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/common/data.py +44 -22
  6. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0/src/cubething_psync.egg-info}/PKG-INFO +1 -1
  7. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/SOURCES.txt +2 -1
  8. cubething_psync-0.4.0/src/server/args.py +107 -0
  9. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/server/main.py +91 -87
  10. cubething_psync-0.4.0/test/test_integration.py +153 -0
  11. cubething_psync-0.2.1.dev2/src/client/args.py +0 -94
  12. cubething_psync-0.2.1.dev2/src/server/args.py +0 -69
  13. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/LICENSE +0 -0
  14. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/README.md +0 -0
  15. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/setup.cfg +0 -0
  16. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/client/__init__.py +0 -0
  17. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/client/__main__.py +0 -0
  18. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/common/__init__.py +0 -0
  19. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/common/log.py +0 -0
  20. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/dependency_links.txt +0 -0
  21. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/entry_points.txt +0 -0
  22. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/requires.txt +0 -0
  23. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/top_level.txt +0 -0
  24. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/server/__init__.py +0 -0
  25. {cubething_psync-0.2.1.dev2 → cubething_psync-0.4.0}/src/server/__main__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.2.1.dev2
3
+ Version: 0.4.0
4
4
  Summary: Simple project synchronization tool.
5
5
  Author-email: ada mandala <ada@cubething.dev>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cubething_psync"
3
- version = "0.2.1.dev2"
3
+ version = "0.4.0"
4
4
  description = "Simple project synchronization tool."
5
5
  authors = [{name = "ada mandala", email="ada@cubething.dev"}]
6
6
  readme = "README.md"
@@ -26,6 +26,23 @@ build-backend = "setuptools.build_meta"
26
26
  [tool.setuptools.packages.find]
27
27
  where= ["src"]
28
28
 
29
+ [dependency-groups]
30
+ dev = [
31
+ "basedpyright>=1.35.0",
32
+ "pytest>=9.0.1",
33
+ "ruff>=0.14.7",
34
+ "testcontainers>=4.13.3",
35
+ ]
36
+
29
37
  [project.scripts]
30
38
  psync-server = "server.main:main"
31
39
  psync-client = "client.main:main"
40
+
41
+ [pytest]
42
+ testpaths="test"
43
+ addopts = ["--import-mode=importlib"]
44
+
45
+ [tool.basedpyright]
46
+ ignore=["**/__pycache__", "build", "dist"]
47
+ include=["src", "test"]
48
+ failOnWarnings=false
@@ -0,0 +1,207 @@
1
+ import argparse
2
+ from dataclasses import dataclass, field
3
+ import hashlib
4
+ import logging
5
+ import os
6
+ from os.path import basename
7
+ from pathlib import Path
8
+ import shlex
9
+ from common.data import deserialize_env
10
+ from typing import TYPE_CHECKING, Any
11
+ if TYPE_CHECKING:
12
+ from _typeshed import SupportsWrite
13
+ Logfile = SupportsWrite[str] | Path | None
14
+ else:
15
+ Logfile = Any
16
+
17
+
18
+ @dataclass
19
+ class Args:
20
+ """
21
+ Client arguments.
22
+ """
23
+
24
+ target_path: str
25
+ """
26
+ ``--path -p <target_path>``
27
+
28
+ **Required.** Path to the target executable.
29
+ """
30
+
31
+ assets: list[str] = field(default_factory=list)
32
+ """
33
+ ``--assets -A <env>``
34
+
35
+ Extra files or directories to be synced to the destination path.
36
+ """
37
+
38
+ env: dict[str, str] = field(default_factory=dict)
39
+ """
40
+ ``--env -e <env>``
41
+
42
+ Space separated list of environment variables which will be passed to the executable.
43
+ """
44
+
45
+ args: list[str] = field(default_factory=list)
46
+ """
47
+ ``--args -a <args>``
48
+
49
+ Arguments passed to the executable.
50
+ """
51
+
52
+ server_ip: str = os.environ.get("PSYNC_SERVER_IP", "127.0.0.1")
53
+ """
54
+ environ: ``PSYNC_SERVER_IP``
55
+
56
+ Server IP address.
57
+ """
58
+
59
+ server_port: int = int(os.environ.get("PSYNC_SERVER_PORT", "5000"))
60
+ """
61
+ environ: ``PSYNC_SERVER_PORT``
62
+
63
+ Server port.
64
+ """
65
+
66
+ server_ssh_port: int = int(os.environ.get("PSYNC_SSH_PORT", "5022"))
67
+ """
68
+ environ: ``PSYNC_SSH_PORT``
69
+
70
+ SSH port on the server host. Client must be authenticated with a shared public key.
71
+ """
72
+
73
+ ssh_args: str = os.environ.get("PSYNC_SSH_ARGS", "-l psync")
74
+ """
75
+ environ: ``PSYNC_SSH_ARGS``
76
+
77
+ Arguments passed to SSH. Under the hood, psync runs
78
+ ``rsync -e "/usr/bin/ssh {PSYNC_SSH_ARGS} -p {PSYNC_SSH_PORT}"``
79
+ """
80
+
81
+ server_dest: str = os.environ.get("PSYNC_SERVER_DEST", "/home/psync")
82
+ """
83
+ environ: ``PSYNC_SERVER_DEST``
84
+
85
+ Base path on the server where the files should be synced.
86
+ """
87
+
88
+ ssl_cert_path: str = os.environ.get(
89
+ "PSYNC_CERT_PATH", "~/.local/share/psync/cert.pem"
90
+ )
91
+ """
92
+ environ: ``PSYNC_CERT_PATH``
93
+
94
+ Public SSL certificate used to trust the psync server.
95
+ """
96
+
97
+ client_origin: str = os.environ.get("PSYNC_CLIENT_ORIGIN", "127.0.0.1")
98
+ """
99
+ environ: ``PSYNC_CLIENT_ORIGIN``
100
+
101
+ Domain name. Should match the origins set in the server's ``PSYNC_ORIGINS``
102
+ variable.
103
+ """
104
+
105
+ logfile: Logfile = None
106
+ """
107
+ environ: ``PSYNC_LOG_FILE``
108
+
109
+ Optional file where the executable's logs will be output.
110
+ """
111
+
112
+ def project_hash(self) -> str:
113
+ """
114
+ Hash value generated from the target path. Used as the directory name for the project.
115
+ """
116
+ return hashlib.blake2s(self.target_path.encode(), digest_size=8).hexdigest()
117
+
118
+ def rsync_url(self) -> str:
119
+ """
120
+ {server_ip}:{server_dest}/{project_hash}
121
+ """
122
+ return f"{self.server_ip}:{self.server_dest}/{self.project_hash()}/"
123
+
124
+ def destination_path(self) -> Path:
125
+ """
126
+ {server_dest}/{project_hash}/{basename(target_path)}
127
+ """
128
+ return Path(self.server_dest) / self.project_hash() / basename(self.target_path)
129
+
130
+
131
+ parser = argparse.ArgumentParser(
132
+ prog="psync-client",
133
+ usage="""\
134
+ Client for the psync server.
135
+
136
+ In addition to the options below, the client is configurable through environment
137
+ variables.
138
+
139
+ Variable | Default
140
+ --------------------+-------------------------------
141
+ PSYNC_SERVER_IP | 127.0.0.1
142
+ PSYNC_SERVER_PORT | 5000
143
+ PSYNC_SSH_PORT | 5022
144
+ PSYNC_SERVER_DEST | /home/psync/
145
+ PSYNC_SSH_ARGS | -l psync
146
+ PSYNC_CERT_PATH | ~/.local/share/psync/cert.pem
147
+ PSYNC_CLIENT_ORIGIN | 127.0.0.1
148
+ PSYNC_LOG_FILE | None (stdout)
149
+
150
+ SSH arguments will be append with "-p {PSYNC_SSH_PORT}"
151
+
152
+ For more info, please read the docs:
153
+ https://psync.readthedocs.io/
154
+ """,
155
+ )
156
+ _action = parser.add_argument(
157
+ "--path",
158
+ "-p",
159
+ required=True,
160
+ help="Path to the target exectuable.",
161
+ )
162
+ _action = parser.add_argument(
163
+ "--assets",
164
+ "-A",
165
+ nargs="+",
166
+ help="Extra files or directories to be synced to the destination path.",
167
+ )
168
+ _action = parser.add_argument(
169
+ "--env",
170
+ "-e",
171
+ help="Environment variables to set in the remote execution environment. Variables must be space-sepated or double-quoted.",
172
+ )
173
+ _action = parser.add_argument(
174
+ "--args", "-a", help="Arguments passed to the executable."
175
+ )
176
+
177
+
178
+ def parse_args() -> Args:
179
+ args = vars(parser.parse_args())
180
+
181
+ target_path = str(args.get("path"))
182
+ target_path = Path(target_path)
183
+ if not target_path.is_file():
184
+ logging.error(f"Could not file at {target_path}")
185
+ exit(1)
186
+
187
+ extra: list[str] = []
188
+ extra_raw = args.get("extra")
189
+ if extra_raw is not None:
190
+ extra = extra_raw # pyright: ignore[reportAny]
191
+
192
+ client_args: list[str] = []
193
+ raw_args = args.get("args")
194
+ if raw_args is not None:
195
+ client_args = shlex.split(str(raw_args)) # pyright: ignore[reportAny]
196
+
197
+ env: dict[str, str] = dict()
198
+ raw_env = args.get("env")
199
+ if raw_env is not None:
200
+ env = deserialize_env(f"env='{raw_env}'")
201
+
202
+ return Args(
203
+ target_path=str(target_path),
204
+ assets=extra or [],
205
+ env=env,
206
+ args=client_args,
207
+ )
@@ -2,14 +2,16 @@
2
2
  psync client
3
3
  """
4
4
 
5
+ from io import TextIOWrapper
6
+ import sys
5
7
  import asyncio
6
- import hashlib
7
8
  import os
8
- import pathlib
9
+ from pathlib import Path
9
10
  import signal
10
11
  import ssl
11
12
  import subprocess
12
13
  import websockets
14
+ from websockets.typing import Origin
13
15
  from common.data import (
14
16
  ErrorResp,
15
17
  ExitResp,
@@ -17,90 +19,92 @@ from common.data import (
17
19
  LogResp,
18
20
  OkayResp,
19
21
  OpenReq,
22
+ SetPidResp,
20
23
  deserialize,
21
24
  serialize,
22
25
  )
23
26
  import logging
24
27
  from common.log import InterceptHandler
25
28
  from client.args import (
26
- SERVER_IP,
27
- SERVER_PORT,
28
- SERVER_SSH_PORT,
29
- USER,
30
- SSL_CERT_PATH,
31
- SERVER_DEST,
32
29
  Args,
33
30
  parse_args,
34
31
  )
35
32
 
33
+ from typing import TYPE_CHECKING, Any
34
+
35
+ if TYPE_CHECKING:
36
+ from _typeshed import SupportsWrite
37
+ Logfile = SupportsWrite[str]
38
+ else:
39
+ Logfile = Any
40
+
36
41
 
37
42
  class PsyncClient:
38
43
  """
39
44
  The primary interface for psync. The client CLI allows users to sync files with
40
45
  rsync, then execute them remotely while receiving the logs.
41
-
42
- CLI arguments: ::
43
- -h, --help show this help message and exit
44
- --path, -p PATH Path to the target exectuable.
45
- --extra, -E EXTRA [EXTRA ...]
46
- Extra files or directories to be synced to the destination path.
47
- --env, -e ENV Environment variables to set in the remote execution environment. Variables
48
- must be space-sepated or double-quoted.
49
- --args, -a ARGS Arguments with which to run the remote executable.
50
-
51
- Environment configuration:
52
- PSYNC_SERVER_IP: The IP address of the server instance.
53
- Default: 127.0.0.1
54
- PSYNC_SERVER_PORT: The port of the server instance.
55
- Default: 5000
56
- PSYNC_SSH_PORT: The server instance's SSH port.
57
- Default: 5022
58
- PSYNC_SSH_USER: The server instance's SSH user.
59
- Default: psync
60
- PSYNC_CERT_PATH: Path to the SSL certificate. Used to trust self-signed certs. Should
61
- match the server's certificate.
62
- Default: ~/.local/share/psync/cert.pem
63
46
  """
64
47
 
65
- __args: list[str]
66
- __env: dict[str, str]
67
- __path: pathlib.Path
68
- __quit: bool = False
48
+ args: Args
49
+ pid: int | None = None
50
+ """ID of the current connection."""
69
51
  __force_exit: bool = False
52
+ __outfile: Logfile
53
+
54
+ def __init__(self, args: Args):
55
+ self.args = args
56
+ if isinstance(self.args.logfile, Path):
57
+ self.__outfile = open(self.args.logfile, "w")
58
+ elif self.args.logfile is not None:
59
+ self.__outfile = self.args.logfile
60
+ else:
61
+ self.__outfile = sys.stdout
70
62
 
71
- def __init__(self, args: list[str], env: dict[str, str], path: pathlib.Path):
72
- self.__args = args
73
- self.__env = env
74
- self.__path = path
63
+ def __enter__(self):
64
+ return self
65
+
66
+ def __exit__(self):
67
+ if isinstance(self.__outfile, TextIOWrapper):
68
+ self.__outfile.close()
75
69
 
76
70
  def __mk_handler(self, ws: websockets.ClientConnection):
77
71
  async def inner():
78
72
  if not self.__force_exit:
79
73
  logging.info("Gracefully shutting down...")
80
74
  self.__force_exit = True
81
- await ws.send(serialize(KillReq()))
75
+ if self.pid is not None:
76
+ await ws.send(serialize(KillReq(pid=self.pid)))
82
77
  await ws.close()
83
78
  asyncio.get_event_loop().stop()
79
+ raise SystemExit(130)
84
80
  else:
85
81
  logging.warning("Got second SIGINT, shutting down")
86
82
  asyncio.get_event_loop().stop()
87
- raise Exception("Forced shutdown")
83
+ raise SystemExit(1)
88
84
 
89
85
  return lambda: asyncio.create_task(inner())
90
86
 
91
87
  async def run(self):
92
88
  """Run the client instance."""
93
89
  ssl_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
94
- ssl_ctx.load_verify_locations(pathlib.Path(SSL_CERT_PATH).expanduser())
90
+ ssl_ctx.load_verify_locations(Path(self.args.ssl_cert_path).expanduser())
95
91
  ssl_ctx.check_hostname = False # not ideal
96
92
  async with websockets.connect(
97
- f"wss://{SERVER_IP}:{SERVER_PORT}", ssl=ssl_ctx
93
+ f"wss://{self.args.server_ip}:{self.args.server_port}",
94
+ ssl=ssl_ctx,
95
+ origin=Origin(f"wss://{self.args.client_origin}"),
98
96
  ) as ws:
99
97
  asyncio.get_event_loop().add_signal_handler(
100
98
  signal.SIGINT, self.__mk_handler(ws)
101
99
  )
102
100
  await ws.send(
103
- serialize(OpenReq(path=self.__path, env=self.__env, args=self.__args))
101
+ serialize(
102
+ OpenReq(
103
+ path=self.args.destination_path(),
104
+ env=self.args.env,
105
+ args=self.args.args,
106
+ )
107
+ )
104
108
  )
105
109
  async for data in ws:
106
110
  if isinstance(data, bytes):
@@ -115,47 +119,50 @@ class PsyncClient:
115
119
  f"Failed to deserialize message '{msg}' with error '{e}'"
116
120
  )
117
121
  await ws.close()
118
- exit(1)
122
+ raise Exception(e)
119
123
 
120
124
  match resp:
121
125
  case LogResp():
122
- print(resp.msg, end="")
126
+ print(resp.msg, end="", file=self.__outfile)
123
127
  case ErrorResp():
124
128
  logging.error(f"Received server error: {resp.msg}")
125
129
  await ws.close()
126
- exit(1)
130
+ raise Exception(resp.msg)
127
131
  case ExitResp():
128
132
  logging.info(f"Exiting with code {resp.exit_code}")
129
133
  await ws.close()
130
- exit(resp.exit_code)
134
+ raise SystemExit(resp.exit_code)
135
+ case SetPidResp():
136
+ logging.info(f"Remote PID = {resp.pid}")
137
+ self.pid = resp.pid
131
138
  case OkayResp():
132
- logging.info("Running exectuable...")
139
+ logging.info("OK.")
133
140
  case _:
134
141
  logging.warning(f"Got unknown request {resp}")
135
142
 
136
143
 
137
- def __rsync(project_hash: str, args: Args):
144
+ def __rsync(args: Args):
138
145
  """Runs rsync."""
139
- url = f"{SERVER_IP}:{SERVER_DEST}/{project_hash}/"
140
146
  rsync_args = [
141
147
  "rsync",
142
148
  "-avzr",
143
149
  "-e",
144
- f"/usr/bin/ssh -l {USER} -p {str(SERVER_SSH_PORT)}",
150
+ f"/usr/bin/ssh {args.ssh_args} -p {str(args.server_ssh_port)}",
145
151
  "--progress",
146
152
  "--mkpath",
147
153
  args.target_path,
148
- *args.extra,
149
- url,
154
+ *args.assets,
155
+ args.rsync_url(),
150
156
  ]
151
157
  logging.info(" ".join(rsync_args))
152
158
  p = subprocess.run(rsync_args)
153
159
  if p.returncode != 0:
154
- logging.error(f"Rsync failed with exit code {p.returncode}")
155
- exit(1)
160
+ msg = f"Rsync failed with exit code {p.returncode}"
161
+ logging.error(msg)
162
+ raise Exception(msg)
156
163
 
157
164
 
158
- def main():
165
+ def main(args: Args | None = None):
159
166
  """
160
167
  The main executable.
161
168
  Sync project files with rsync, then run the client.
@@ -163,15 +170,15 @@ def main():
163
170
  log_level = os.environ.get("PSYNC_LOG", "INFO").upper()
164
171
  logging.basicConfig(handlers=[InterceptHandler()], level=log_level, force=True)
165
172
 
166
- project_hash = hashlib.blake2s(os.getcwd().encode(), digest_size=8).hexdigest()
167
- args = parse_args()
168
- __rsync(project_hash, args)
173
+ args = parse_args() if args is None else args
174
+ __rsync(args)
169
175
 
170
- dest_path = pathlib.Path(
171
- f"{SERVER_DEST}/{project_hash}/{os.path.basename(args.target_path)}"
172
- )
173
- client = PsyncClient(args=args.args, env=args.env, path=dest_path)
174
- asyncio.run(client.run())
176
+ try:
177
+ asyncio.run(PsyncClient(args).run())
178
+ except SystemExit as e:
179
+ exit(e.code)
180
+ except Exception:
181
+ exit(1)
175
182
 
176
183
 
177
184
  if __name__ == "__main__":
@@ -1,4 +1,3 @@
1
- from collections.abc import Mapping
2
1
  from dataclasses import dataclass
3
2
  import logging
4
3
  from pathlib import Path
@@ -15,6 +14,7 @@ class Mode(Enum):
15
14
  class ReqKind(Enum):
16
15
  Open = "open"
17
16
  Kill = "kill"
17
+ HealthCheck = "hc"
18
18
 
19
19
 
20
20
  class RespKind(Enum):
@@ -22,18 +22,20 @@ class RespKind(Enum):
22
22
  Error = "error"
23
23
  Exit = "exit"
24
24
  Okay = "ok"
25
+ SetPid = "set_pid"
25
26
 
26
27
 
27
28
  @dataclass
28
29
  class OpenReq:
29
30
  path: Path
30
31
  args: list[str]
31
- env: Mapping[str, str]
32
+ env: dict[str,str]
32
33
  kind: ReqKind = ReqKind.Open
33
34
 
34
35
 
35
36
  @dataclass
36
37
  class KillReq:
38
+ pid: int
37
39
  kind: ReqKind = ReqKind.Kill
38
40
 
39
41
 
@@ -60,28 +62,44 @@ class OkayResp:
60
62
  kind: RespKind = RespKind.Okay
61
63
 
62
64
 
63
- Req = OpenReq | KillReq
64
- Resp = LogResp | ExitResp | ErrorResp | OkayResp
65
+ @dataclass
66
+ class HealthCheckReq:
67
+ kind: ReqKind = ReqKind.HealthCheck
68
+
69
+
70
+ @dataclass
71
+ class SetPidResp:
72
+ pid: int
73
+ kind: RespKind = RespKind.SetPid
74
+
75
+
76
+ Req = OpenReq | KillReq | HealthCheckReq
77
+ Resp = LogResp | ExitResp | ErrorResp | OkayResp | SetPidResp
65
78
 
66
79
 
67
80
  def serialize(msg: Req | Resp) -> str:
81
+ value = msg.kind.value
68
82
  match msg:
69
83
  case OpenReq():
70
84
  args = " ".join(msg.args)
71
85
  env: list[str] = []
72
- for key, value in msg.env.items():
73
- env.append(f'{key}="{value}"')
74
- return f"open path='{msg.path}' args='{args}' env='{' '.join(env)}'"
86
+ for k, v in msg.env.items():
87
+ env.append(f'{k}="{v}"')
88
+ return f"{value} path='{msg.path}' args='{args}' env='{' '.join(env)}'"
75
89
  case KillReq():
76
- return "kill"
90
+ return f"{value} {msg.pid}"
91
+ case SetPidResp():
92
+ return f"{value} {msg.pid}"
77
93
  case LogResp():
78
- return f"log {msg.msg}"
94
+ return f"{value} {msg.msg}"
79
95
  case ExitResp():
80
- return f"exit {msg.exit_code}"
96
+ return f"{value} {msg.exit_code}"
81
97
  case ErrorResp():
82
- return f"error {msg.msg}"
98
+ return f"{value} {msg.msg}"
83
99
  case OkayResp():
84
- return "okay"
100
+ return f"{value}"
101
+ case HealthCheckReq():
102
+ return f"{value}"
85
103
 
86
104
 
87
105
  path_expr = re.compile(r"path='([^']+)'")
@@ -106,13 +124,9 @@ def deserialize(msg: str) -> Req | Resp:
106
124
  logging.debug(f"Got message {msg}")
107
125
  try:
108
126
  [kind, rest] = msg.split(" ", 1)
109
- except Exception as e:
110
- if msg == "okay":
111
- return OkayResp()
112
- elif msg == "kill":
113
- return KillReq()
114
- else:
115
- raise e
127
+ except Exception:
128
+ kind = msg.strip()
129
+ rest = ""
116
130
 
117
131
  match kind:
118
132
  case ReqKind.Open.value:
@@ -134,12 +148,20 @@ def deserialize(msg: str) -> Req | Resp:
134
148
 
135
149
  return OpenReq(path=Path(path), args=args, env=env)
136
150
 
151
+ case ReqKind.Kill.value:
152
+ return KillReq(int(rest))
137
153
  case RespKind.Log.value:
138
- return LogResp(msg=rest)
154
+ return LogResp(rest)
139
155
  case RespKind.Exit.value:
140
- return ExitResp(exit_code=rest)
156
+ return ExitResp(rest)
141
157
  case RespKind.Error.value:
142
- return ErrorResp(msg=rest)
158
+ return ErrorResp(rest)
159
+ case RespKind.Okay.value:
160
+ return OkayResp()
161
+ case ReqKind.HealthCheck.value:
162
+ return HealthCheckReq()
163
+ case RespKind.SetPid.value:
164
+ return SetPidResp(int(rest))
143
165
 
144
166
  case _:
145
167
  raise ValueError("Could not match kind for message", msg)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.2.1.dev2
3
+ Version: 0.4.0
4
4
  Summary: Simple project synchronization tool.
5
5
  Author-email: ada mandala <ada@cubething.dev>
6
6
  License-Expression: MIT
@@ -17,4 +17,5 @@ src/cubething_psync.egg-info/top_level.txt
17
17
  src/server/__init__.py
18
18
  src/server/__main__.py
19
19
  src/server/args.py
20
- src/server/main.py
20
+ src/server/main.py
21
+ test/test_integration.py