cubething_psync 0.3.0__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 (24) hide show
  1. {cubething_psync-0.3.0/src/cubething_psync.egg-info → cubething_psync-0.4.0}/PKG-INFO +1 -1
  2. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/pyproject.toml +6 -1
  3. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/client/args.py +17 -0
  4. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/client/main.py +25 -1
  5. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/common/data.py +4 -4
  6. {cubething_psync-0.3.0 → cubething_psync-0.4.0/src/cubething_psync.egg-info}/PKG-INFO +1 -1
  7. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/server/main.py +15 -7
  8. cubething_psync-0.4.0/test/test_integration.py +153 -0
  9. cubething_psync-0.3.0/test/test_integration.py +0 -83
  10. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/LICENSE +0 -0
  11. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/README.md +0 -0
  12. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/setup.cfg +0 -0
  13. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/client/__init__.py +0 -0
  14. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/client/__main__.py +0 -0
  15. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/common/__init__.py +0 -0
  16. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/common/log.py +0 -0
  17. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/SOURCES.txt +0 -0
  18. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/dependency_links.txt +0 -0
  19. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/entry_points.txt +0 -0
  20. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/requires.txt +0 -0
  21. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/cubething_psync.egg-info/top_level.txt +0 -0
  22. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/server/__init__.py +0 -0
  23. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/server/__main__.py +0 -0
  24. {cubething_psync-0.3.0 → cubething_psync-0.4.0}/src/server/args.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.3.0
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.3.0"
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"
@@ -41,3 +41,8 @@ psync-client = "client.main:main"
41
41
  [pytest]
42
42
  testpaths="test"
43
43
  addopts = ["--import-mode=importlib"]
44
+
45
+ [tool.basedpyright]
46
+ ignore=["**/__pycache__", "build", "dist"]
47
+ include=["src", "test"]
48
+ failOnWarnings=false
@@ -7,6 +7,12 @@ from os.path import basename
7
7
  from pathlib import Path
8
8
  import shlex
9
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
10
16
 
11
17
 
12
18
  @dataclass
@@ -96,6 +102,13 @@ class Args:
96
102
  variable.
97
103
  """
98
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
+
99
112
  def project_hash(self) -> str:
100
113
  """
101
114
  Hash value generated from the target path. Used as the directory name for the project.
@@ -132,8 +145,12 @@ PSYNC_SERVER_DEST | /home/psync/
132
145
  PSYNC_SSH_ARGS | -l psync
133
146
  PSYNC_CERT_PATH | ~/.local/share/psync/cert.pem
134
147
  PSYNC_CLIENT_ORIGIN | 127.0.0.1
148
+ PSYNC_LOG_FILE | None (stdout)
135
149
 
136
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/
137
154
  """,
138
155
  )
139
156
  _action = parser.add_argument(
@@ -2,6 +2,8 @@
2
2
  psync client
3
3
  """
4
4
 
5
+ from io import TextIOWrapper
6
+ import sys
5
7
  import asyncio
6
8
  import os
7
9
  from pathlib import Path
@@ -28,6 +30,14 @@ from client.args import (
28
30
  parse_args,
29
31
  )
30
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
+
31
41
 
32
42
  class PsyncClient:
33
43
  """
@@ -39,9 +49,23 @@ class PsyncClient:
39
49
  pid: int | None = None
40
50
  """ID of the current connection."""
41
51
  __force_exit: bool = False
52
+ __outfile: Logfile
42
53
 
43
54
  def __init__(self, args: Args):
44
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
62
+
63
+ def __enter__(self):
64
+ return self
65
+
66
+ def __exit__(self):
67
+ if isinstance(self.__outfile, TextIOWrapper):
68
+ self.__outfile.close()
45
69
 
46
70
  def __mk_handler(self, ws: websockets.ClientConnection):
47
71
  async def inner():
@@ -99,7 +123,7 @@ class PsyncClient:
99
123
 
100
124
  match resp:
101
125
  case LogResp():
102
- print(resp.msg, end="")
126
+ print(resp.msg, end="", file=self.__outfile)
103
127
  case ErrorResp():
104
128
  logging.error(f"Received server error: {resp.msg}")
105
129
  await ws.close()
@@ -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
@@ -30,7 +29,7 @@ class RespKind(Enum):
30
29
  class OpenReq:
31
30
  path: Path
32
31
  args: list[str]
33
- env: Mapping[str, str]
32
+ env: dict[str,str]
34
33
  kind: ReqKind = ReqKind.Open
35
34
 
36
35
 
@@ -84,8 +83,8 @@ def serialize(msg: Req | Resp) -> str:
84
83
  case OpenReq():
85
84
  args = " ".join(msg.args)
86
85
  env: list[str] = []
87
- for key, value in msg.env.items():
88
- env.append(f'{key}="{value}"')
86
+ for k, v in msg.env.items():
87
+ env.append(f'{k}="{v}"')
89
88
  return f"{value} path='{msg.path}' args='{args}' env='{' '.join(env)}'"
90
89
  case KillReq():
91
90
  return f"{value} {msg.pid}"
@@ -127,6 +126,7 @@ def deserialize(msg: str) -> Req | Resp:
127
126
  [kind, rest] = msg.split(" ", 1)
128
127
  except Exception:
129
128
  kind = msg.strip()
129
+ rest = ""
130
130
 
131
131
  match kind:
132
132
  case ReqKind.Open.value:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cubething_psync
3
- Version: 0.3.0
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
@@ -9,7 +9,6 @@ import asyncio
9
9
  from asyncio.tasks import Task
10
10
  from asyncio.subprocess import Process
11
11
  from collections.abc import Awaitable
12
- from os.path import basename
13
12
  import pathlib
14
13
  import signal
15
14
  import ssl
@@ -166,10 +165,18 @@ class PsyncServer:
166
165
  async def __open(self, req: OpenReq, ws: ServerConnection):
167
166
  host = self.__get_host(ws)
168
167
  path = pathlib.Path.expanduser(req.path).resolve()
169
- args = [str(path), *req.args]
170
- env = req.env if not self.args.use_base_env else {**environ, **req.env}
171
-
172
- info_log = f"Running `[...]/{basename(args[0])} {' '.join(args[1:])}`..."
168
+ base_env = environ.copy() if self.args.use_base_env else {}
169
+ if not self.args.use_base_env:
170
+ # still get path and etc
171
+ for var in ['PATH','HOME','USER','SHELL']:
172
+ if var in environ:
173
+ base_env[var] = environ[var]
174
+ if 'VIRTUAL_ENV' in environ:
175
+ base_env['VIRUTAL_ENV'] = environ['VIRTUAL_ENV']
176
+ base_env["PATH"] = f"{environ['VIRTUAL_ENV']}/bin:{base_env["PATH"]}"
177
+ env = base_env | req.env
178
+
179
+ info_log = f"Running `{path} {' '.join(req.args)}`"
173
180
  if env != {}:
174
181
  info_log += f"\n... with env {pprint(env)}"
175
182
  if self.args.user is not None:
@@ -179,7 +186,8 @@ class PsyncServer:
179
186
 
180
187
  try:
181
188
  p = await asyncio.create_subprocess_exec(
182
- *args,
189
+ path,
190
+ *req.args,
183
191
  env=env,
184
192
  stdout=asyncio.subprocess.PIPE,
185
193
  stderr=asyncio.subprocess.STDOUT,
@@ -196,7 +204,7 @@ class PsyncServer:
196
204
  await ws.send(serialize(resp))
197
205
 
198
206
  except Exception as e:
199
- logging.error(f"Failed to start process `{args[0]}` with error {e}")
207
+ logging.error(f"Failed to start process `{path}` with error {e}")
200
208
  resp = ErrorResp(f"Server error: {e}")
201
209
  await ws.send(serialize(resp))
202
210
 
@@ -0,0 +1,153 @@
1
+ import asyncio
2
+ import multiprocessing
3
+ import os
4
+ from os.path import basename
5
+ import re
6
+ import sys
7
+ from signal import Signals
8
+ from io import StringIO
9
+ from dataclasses import dataclass
10
+
11
+ from testcontainers.core.generic import DockerContainer # pyright: ignore[reportMissingTypeStubs]
12
+
13
+ from client.args import Args as ClientArgs
14
+ from client.main import PsyncClient
15
+ from client.main import __rsync as rsync # pyright: ignore[reportPrivateUsage]
16
+ from test.conftest import assets_path
17
+
18
+ OUTFILE = StringIO()
19
+
20
+
21
+ @dataclass
22
+ class Logs:
23
+ stdout: str
24
+ stderr: str
25
+
26
+
27
+ def get_logs(server: DockerContainer) -> Logs:
28
+ slogs = server.get_logs()
29
+ return Logs(stdout=slogs[0].decode(), stderr=slogs[1].decode())
30
+
31
+
32
+ def template(args: ClientArgs, server: DockerContainer, kill: bool = False):
33
+ try:
34
+ rsync(args)
35
+
36
+ def find(path: str):
37
+ exec_result = server.exec(["ls", str(path)])
38
+ ok = exec_result.output.decode().__contains__(basename(args.target_path))
39
+ if not ok:
40
+ print(f"Could not find path {path}")
41
+ assert False
42
+
43
+ find(str(args.destination_path()))
44
+ for file in args.assets:
45
+ find(args.target_path + "/" + file)
46
+
47
+ client = PsyncClient(args)
48
+
49
+ def run(code: int):
50
+ try:
51
+ asyncio.run(client.run())
52
+ except SystemExit as e:
53
+ assert str(e.code) == str(code)
54
+
55
+ if not kill:
56
+ run(0)
57
+ else:
58
+ p = multiprocessing.Process(target=run, args=[130])
59
+ p.start()
60
+ while p.pid is None:
61
+ pass
62
+ asyncio.run(asyncio.sleep(1))
63
+ os.kill(p.pid, Signals.SIGINT)
64
+ p.join(3)
65
+
66
+ # check that the pid closed
67
+ logs = get_logs(server)
68
+
69
+ pat = re.compile(r"Running process with PID (\d+)")
70
+ res = pat.search(logs.stderr)
71
+ if res is None:
72
+ raise Exception("Could not get PID from stdout!")
73
+ pid = res.group(1)
74
+
75
+ client_logs = OUTFILE.getvalue()
76
+
77
+ if args.args != []:
78
+ pat = re.compile(r"argv1=\w+")
79
+ res = pat.search(client_logs)
80
+ if res is None:
81
+ raise Exception("Did not find argv1 value!")
82
+
83
+ if args.env.get("TEST") is not None:
84
+ pat = re.compile(r"'TEST':\s*'TEST'")
85
+ res = pat.search(client_logs)
86
+ if res is None:
87
+ raise Exception("Env value TEST was not set!")
88
+
89
+ exec_result = server.exec(
90
+ ["sh", "-c", f"ps -p {pid} > /dev/null; echo $?"],
91
+ )
92
+ assert exec_result.output.decode().strip() == "1"
93
+
94
+ except Exception as e:
95
+ print(f"Got exception:\n {e}", file=sys.stderr)
96
+ logs = get_logs(server)
97
+ print(
98
+ f"Server logs:\n--- stdout ---\n{logs.stdout}\n--- stderr ---\n{logs.stderr}",
99
+ file=sys.stderr,
100
+ )
101
+ print(
102
+ f"OUTFILE:\n--- stdout ---\n{OUTFILE.getvalue()}",
103
+ file=sys.stderr,
104
+ )
105
+ assert False
106
+
107
+
108
+ def get_test_args(file: str, server: DockerContainer):
109
+ return ClientArgs(
110
+ target_path=assets_path.joinpath(file).__str__(),
111
+ ssh_args=f"-i {(assets_path / 'ssh-key').resolve()} -l psync -o StrictHostKeyChecking=no",
112
+ ssl_cert_path=(assets_path / "cert.pem").resolve().__str__(),
113
+ server_ip="127.0.0.1",
114
+ server_port=server.get_exposed_port(5000),
115
+ server_ssh_port=server.get_exposed_port(5022),
116
+ logfile=OUTFILE,
117
+ )
118
+
119
+
120
+ def test_basic(server: DockerContainer):
121
+ args = get_test_args("example_basic.py", server)
122
+ template(args, server)
123
+
124
+
125
+ def test_sigint(server: DockerContainer):
126
+ args = get_test_args("example.py", server)
127
+ template(args, server, True)
128
+
129
+
130
+ def test_env(server: DockerContainer):
131
+ args = get_test_args("example_basic.py", server)
132
+ args.env = {"PYTHONUNBUFERED": "1", "TEST": "TEST"}
133
+ template(args, server)
134
+
135
+
136
+ def test_assets(server: DockerContainer):
137
+ args = get_test_args("example_basic.py", server)
138
+ args.assets = ["./test/assets/wizard.png", "./test/assets/test-dir"]
139
+ template(args, server)
140
+
141
+
142
+ def test_args(server: DockerContainer):
143
+ args = get_test_args("example_basic.py", server)
144
+ args.args = ["test"]
145
+ template(args, server)
146
+
147
+
148
+ def test_full(server: DockerContainer):
149
+ args = get_test_args("example_basic.py", server)
150
+ args.args = ["test"]
151
+ args.assets = ["./test/assets/wizard.png", "./test/assets/test-dir"]
152
+ args.env = {"PYTHONUNBUFERED": "1", "TEST": "TEST"}
153
+ template(args, server)
@@ -1,83 +0,0 @@
1
- import asyncio
2
- import multiprocessing
3
- import os
4
- from os.path import basename
5
- import re
6
- import sys
7
- from signal import Signals
8
-
9
- from testcontainers.core.generic import DockerContainer # pyright: ignore[reportMissingTypeStubs]
10
-
11
- from client.args import Args as ClientArgs
12
- from client.main import PsyncClient
13
- from client.main import __rsync as rsync # pyright: ignore[reportPrivateUsage]
14
- from test.conftest import assets_path
15
-
16
-
17
- def template(args: ClientArgs, server: DockerContainer, kill: bool = False):
18
- try:
19
- rsync(args)
20
- exec_result = server.exec(["ls", str(args.destination_path())])
21
- print(f"ls {args.destination_path()}\n --- \n {exec_result.output}")
22
- assert exec_result.output.decode().__contains__(basename(args.target_path))
23
- client = PsyncClient(args)
24
-
25
- def run(code: int):
26
- try:
27
- asyncio.run(client.run())
28
- except SystemExit as e:
29
- assert str(e.code) == str(code)
30
-
31
- if not kill:
32
- run(0)
33
- else:
34
- p = multiprocessing.Process(target=run, args=[130])
35
- p.start()
36
- while p.pid is None:
37
- pass
38
- asyncio.run(asyncio.sleep(1))
39
- os.kill(p.pid, Signals.SIGINT)
40
- p.join(3)
41
-
42
- # check that the pid closed
43
- stdout, stderr = server.get_logs()
44
- pat = re.compile(r"Running process with PID (\d+)")
45
- res = pat.search(stderr.decode())
46
- if res is None:
47
- raise Exception("Could not get PID from stdout!")
48
- pid = res.group(1)
49
-
50
- exec_result = server.exec(
51
- ["sh", "-c", f"ps -p {pid} > /dev/null; echo $?"],
52
- )
53
- assert exec_result.output.decode().strip() == "1"
54
-
55
- except Exception as e:
56
- print(f"Got exception:\n {e}", file=sys.stderr)
57
- stdout, stderr = server.get_logs()
58
- print(
59
- f"Server logs:\n--- stdout ---\n{stdout.decode()}\n--- stderr ---\n{stderr.decode()}",
60
- file=sys.stderr,
61
- )
62
- assert False
63
-
64
-
65
- def get_test_args(file: str, server: DockerContainer):
66
- return ClientArgs(
67
- target_path=assets_path.joinpath(file).__str__(),
68
- ssh_args=f"-i {(assets_path / 'ssh-key').resolve()} -l psync -o StrictHostKeyChecking=no",
69
- ssl_cert_path=(assets_path / "cert.pem").resolve().__str__(),
70
- server_ip="127.0.0.1",
71
- server_port=server.get_exposed_port(5000),
72
- server_ssh_port=server.get_exposed_port(5022),
73
- )
74
-
75
-
76
- def test_basic(server: DockerContainer):
77
- args = get_test_args("example_basic.py", server)
78
- template(args, server)
79
-
80
-
81
- def test_sigint(server: DockerContainer):
82
- args = get_test_args("example.py", server)
83
- template(args, server, True)
File without changes