fujin-cli 0.9.0__py3-none-any.whl → 0.10.0__py3-none-any.whl

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.

Potentially problematic release.


This version of fujin-cli might be problematic. Click here for more details.

fujin/commands/_base.py CHANGED
@@ -10,8 +10,8 @@ from fujin.connection import Connection
10
10
  from fujin.connection import host_connection
11
11
  from fujin.errors import ImproperlyConfiguredError
12
12
  from fujin.hooks import HookManager
13
- from fujin.process_managers import ProcessManager
14
13
  from fujin.proxies import WebProxy
14
+ from fujin.systemd import ProcessManager
15
15
 
16
16
 
17
17
  @dataclass
@@ -66,15 +66,5 @@ class BaseCommand:
66
66
  def create_web_proxy(self, conn: Connection) -> WebProxy:
67
67
  return self.web_proxy_class.create(conn=conn, config=self.config)
68
68
 
69
- @cached_property
70
- def process_manager_class(self) -> type[ProcessManager]:
71
- module = importlib.import_module(self.config.process_manager)
72
- try:
73
- return getattr(module, "ProcessManager")
74
- except KeyError as e:
75
- raise ImproperlyConfiguredError(
76
- f"Missing ProcessManager class in {self.config.process_manager}"
77
- ) from e
78
-
79
69
  def create_process_manager(self, conn: Connection) -> ProcessManager:
80
- return self.process_manager_class.create(conn=conn, config=self.config)
70
+ return ProcessManager.create(conn=conn, config=self.config)
fujin/commands/prune.py CHANGED
@@ -8,7 +8,7 @@ from fujin.commands import BaseCommand
8
8
 
9
9
 
10
10
  @cappa.command(
11
- help="Prune old version artifacts, keeping only the specified number of recent versions"
11
+ help="Prune old artifacts, keeping only the specified number of recent versions"
12
12
  )
13
13
  @dataclass
14
14
  class Prune(BaseCommand):
fujin/config.py CHANGED
@@ -55,6 +55,7 @@ The secret management service to use. Available options:
55
55
 
56
56
  - ``bitwarden``
57
57
  - ``1password``
58
+ - ``doppler``
58
59
 
59
60
  password_env
60
61
  ~~~~~~~~~~~~
@@ -116,6 +117,11 @@ Example:
116
117
  .. note::
117
118
 
118
119
  Commands are relative to your ``app_dir``. When generating systemd service files, the full path is automatically constructed.
120
+ Here are the templates for the service files:
121
+
122
+ - `web.service <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/web.service>`_
123
+ - `web.socket <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/web.socket>`_
124
+ - `simple.service <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/simple.service>`_ (for all additional processes)
119
125
 
120
126
  Host Configuration
121
127
  -------------------
@@ -216,6 +222,7 @@ class InstallationMode(StrEnum):
216
222
  class SecretAdapter(StrEnum):
217
223
  BITWARDEN = "bitwarden"
218
224
  ONE_PASSWORD = "1password"
225
+ DOPPLER = "doppler"
219
226
 
220
227
 
221
228
  class SecretConfig(msgspec.Struct):
@@ -235,7 +242,6 @@ class Config(msgspec.Struct, kw_only=True):
235
242
  aliases: dict[str, str] = msgspec.field(default_factory=dict)
236
243
  host: HostConfig
237
244
  processes: dict[str, str] = msgspec.field(default_factory=dict)
238
- process_manager: str = "fujin.process_managers.systemd"
239
245
  webserver: Webserver
240
246
  requirements: str | None = None
241
247
  hooks: HooksDict = msgspec.field(default_factory=dict)
fujin/proxies/caddy.py CHANGED
@@ -166,7 +166,8 @@ class WebProxy(msgspec.Struct):
166
166
  new_routes = [r for r in existing_routes if r.get("group") != self.app_name]
167
167
  current_config["routes"] = new_routes
168
168
  self.conn.run(
169
- f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'"
169
+ f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'",
170
+ hide="out",
170
171
  )
171
172
 
172
173
  def start(self) -> None:
fujin/secrets/__init__.py CHANGED
@@ -10,6 +10,7 @@ from dotenv import dotenv_values
10
10
  from fujin.config import SecretAdapter
11
11
  from fujin.config import SecretConfig
12
12
  from .bitwarden import bitwarden
13
+ from .dopppler import doppler
13
14
  from .onepassword import one_password
14
15
 
15
16
  secret_reader = Callable[[str], str]
@@ -18,6 +19,7 @@ secret_adapter_context = Callable[[SecretConfig], ContextManager[secret_reader]]
18
19
  adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
19
20
  SecretAdapter.BITWARDEN: bitwarden,
20
21
  SecretAdapter.ONE_PASSWORD: one_password,
22
+ SecretAdapter.DOPPLER: doppler,
21
23
  }
22
24
 
23
25
 
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ from contextlib import contextmanager
5
+ from typing import Generator
6
+ from typing import TYPE_CHECKING
7
+
8
+ import cappa
9
+
10
+ from fujin.config import SecretConfig
11
+
12
+ if TYPE_CHECKING:
13
+ from . import secret_reader
14
+
15
+
16
+ @contextmanager
17
+ def doppler(_: SecretConfig) -> Generator[secret_reader, None, None]:
18
+ def read_secret(name: str) -> str:
19
+ result = subprocess.run(
20
+ ["doppler", "run", "--command", f"echo ${name}"],
21
+ capture_output=True,
22
+ text=True,
23
+ )
24
+ if result.returncode != 0:
25
+ raise cappa.Exit(result.stderr)
26
+ return result.stdout.strip()
27
+
28
+ try:
29
+ yield read_secret
30
+ finally:
31
+ pass
@@ -4,6 +4,8 @@ import importlib.util
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
 
7
+ import gevent
8
+
7
9
  from fujin.config import Config
8
10
  from fujin.connection import Connection
9
11
 
@@ -54,11 +56,23 @@ class ProcessManager:
54
56
  hide="out",
55
57
  )
56
58
 
59
+ threads = []
57
60
  for name in self.processes:
58
61
  if name == "web" and self.is_using_unix_socket:
59
- self.run_pty(f"sudo systemctl enable --now {self.app_name}.socket")
62
+ threads.append(
63
+ gevent.spawn(
64
+ self.run_pty,
65
+ f"sudo systemctl enable --now {self.app_name}.socket",
66
+ )
67
+ )
60
68
  else:
61
- self.run_pty(f"sudo systemctl enable {self.get_service_name(name)}")
69
+ threads.append(
70
+ gevent.spawn(
71
+ self.run_pty,
72
+ f"sudo systemctl enable {self.get_service_name(name)}",
73
+ )
74
+ )
75
+ gevent.joinall(threads)
62
76
 
63
77
  def get_configuration_files(
64
78
  self, ignore_local: bool = False
@@ -105,47 +119,68 @@ class ProcessManager:
105
119
 
106
120
  def uninstall_services(self) -> None:
107
121
  self.stop_services()
108
- for name in self.service_names:
109
- self.run_pty(f"sudo systemctl disable {name}", warn=True)
110
- self.run_pty(f"sudo rm /etc/systemd/system/{name}", warn=True)
122
+ threads = [
123
+ gevent.spawn(self.run_pty, f"sudo systemctl disable {name}", warn=True)
124
+ for name in self.service_names
125
+ ]
126
+ gevent.joinall(threads)
127
+ files_to_delete = [f"/etc/systemd/system/{name}" for name in self.service_names]
128
+ self.run_pty(f"sudo rm {' '.join(files_to_delete)}", warn=True)
111
129
 
112
130
  def start_services(self, *names) -> None:
113
131
  names = names or self.service_names
114
- for name in names:
115
- if name in self.service_names:
116
- self.run_pty(f"sudo systemctl start {name}")
132
+ threads = [
133
+ gevent.spawn(self.run_pty, f"sudo systemctl start {name}")
134
+ for name in names
135
+ if name in self.service_names
136
+ ]
137
+ gevent.joinall(threads)
117
138
 
118
139
  def restart_services(self, *names) -> None:
119
140
  names = names or self.service_names
120
- for name in names:
121
- if name in self.service_names:
122
- self.run_pty(f"sudo systemctl restart {name}")
141
+ threads = [
142
+ gevent.spawn(self.run_pty, f"sudo systemctl restart {name}")
143
+ for name in names
144
+ if name in self.service_names
145
+ ]
146
+ gevent.joinall(threads)
123
147
 
124
148
  def stop_services(self, *names) -> None:
125
149
  names = names or self.service_names
126
- for name in names:
127
- if name in self.service_names:
128
- self.run_pty(f"sudo systemctl stop {name}")
150
+ threads = [
151
+ gevent.spawn(self.run_pty, f"sudo systemctl stop {name}")
152
+ for name in names
153
+ if name in self.service_names
154
+ ]
155
+ gevent.joinall(threads)
129
156
 
130
157
  def is_enabled(self, *names) -> dict[str, bool]:
131
158
  names = names or self.service_names
132
- return {
133
- name: self.run_pty(
134
- f"sudo systemctl is-enabled {name}", warn=True, hide=True
135
- ).stdout.strip()
136
- == "enabled"
159
+ threads = {
160
+ name: gevent.spawn(
161
+ self.run_pty, f"sudo systemctl is-enabled {name}", warn=True, hide=True
162
+ )
137
163
  for name in names
138
164
  }
165
+ gevent.joinall(threads.values())
166
+ return {
167
+ name: thread.value.stdout.strip() == "enabled"
168
+ for name, thread in threads.items()
169
+ }
139
170
 
140
171
  def is_active(self, *names) -> dict[str, bool]:
141
172
  names = names or self.service_names
142
- return {
143
- name: self.run_pty(
144
- f"sudo systemctl is-active {name}", warn=True, hide=True
145
- ).stdout.strip()
146
- == "active"
173
+ threads = {
174
+ name: gevent.spawn(
175
+ self.run_pty, f"sudo systemctl is-active {name}", warn=True, hide=True
176
+ )
147
177
  for name in names
148
178
  }
179
+ gevent.joinall(threads.values())
180
+ return {
181
+ name: thread.value.stdout.strip() == "active"
182
+ for name, thread in threads.items()
183
+ }
149
184
 
150
185
  def service_logs(self, name: str, follow: bool = False):
151
186
  # TODO: add more options here
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fujin-cli
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Get your project up and running in a few minutes on your own vps.
5
5
  Project-URL: Documentation, https://github.com/falcopackages/fujin#readme
6
6
  Project-URL: Issues, https://github.com/falcopackages/fujin/issues
@@ -1,11 +1,12 @@
1
1
  fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  fujin/__main__.py,sha256=VJMBzuQuxkQaAKNEySktGnms944bkRsrIAjj-XeaWR8,1813
3
- fujin/config.py,sha256=LjgOTBumgOQTHkgXLJ0Rb2ERz_WBO-8rDj1hTYo-z0s,11027
3
+ fujin/config.py,sha256=xh7yAStKcCx2bcIgzK0-ocLMtiqsgUtar9xqpqKQKww,11403
4
4
  fujin/connection.py,sha256=LL7LhX9p0X9FmiGdlSroD3Ht216QY0Kd51xkSrXmM3s,2479
5
5
  fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
6
6
  fujin/hooks.py,sha256=EDVYNozlDJ5kc1xHtZrXgtuKplUMEMPTp65TLMP02Ek,1449
7
+ fujin/systemd.py,sha256=aWfN3CDl_NACjsfzyiVmO2h0-VTgc7y3g8RMG2jVsb4,6640
7
8
  fujin/commands/__init__.py,sha256=g0b13vzidPUbxne_Zo_Wv5jpEmwCiSK4AokCinH9uo4,39
8
- fujin/commands/_base.py,sha256=Rtf7kcO_Pg2JxPNKsXEx0UdhqfzYU6g7IiezC8cqEPQ,2610
9
+ fujin/commands/_base.py,sha256=2k9HGcxskFZQ7qYw8hcJh_73EZ_qVngLot7lOo2hQuw,2193
9
10
  fujin/commands/app.py,sha256=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
10
11
  fujin/commands/config.py,sha256=WymGla-H2yduhLcYE1Nb6IJaFIj0S0KIhV9fBDCKsAw,2779
11
12
  fujin/commands/deploy.py,sha256=AylcVNX47ekf-AFfQrYcSh86I3qltADEgjPtV9lQAfE,5831
@@ -14,25 +15,24 @@ fujin/commands/down.py,sha256=aw_mxl_TMC66plKTXwlYP1W2XQBmHeROltQqOpQssyE,1577
14
15
  fujin/commands/init.py,sha256=AHAZeYkSk-zShF288Zs4rCfVwalCUqeMXWR6GTks2v0,4046
15
16
  fujin/commands/printenv.py,sha256=bS2mIgk7zd_w3yDhZLTa1PkHIzuSbBWjnYyM8CUjX0k,523
16
17
  fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
17
- fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
18
+ fujin/commands/prune.py,sha256=3Y0ZpgoI8eb_FVD6XMmZK9lEp2delKUeJdK09kv9_08,1509
18
19
  fujin/commands/redeploy.py,sha256=491Mzz0qiaHqcI7BFUtZq-M34WQkiBd2ZgQbLRLp8T8,2355
19
20
  fujin/commands/rollback.py,sha256=JsocJzQcdQelSnYD94klhjBh8UKkkdiRD9shfUfo4FI,2032
20
21
  fujin/commands/server.py,sha256=-3-PyBNR0fGm-RYE3fz50kP-LSDYGU9BzUxrbGZEghc,3312
21
22
  fujin/commands/up.py,sha256=OEK_n-6-mnnIUffFpR7QtVunr1V1F04pxlAAS1U62BY,419
22
- fujin/process_managers/__init__.py,sha256=MhhfTBhm64zWRAKgjvsZRIToOUJus60vGScbAjqpQ6Y,994
23
- fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAPBObk,5603
24
23
  fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
25
- fujin/proxies/caddy.py,sha256=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
24
+ fujin/proxies/caddy.py,sha256=H52D7cGEEGcxDaXxvClnny9lAat_h1G9dYlIlf6gwKo,7933
26
25
  fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
27
26
  fujin/proxies/nginx.py,sha256=BNJNLxLLRVAmBIGVCk8pb16iiSJsOI9jXOZhdSQGtX8,4151
28
- fujin/secrets/__init__.py,sha256=hiNZIBtOwM0WQbvAF3GhuMMCsQCjXjiSr5gTLAGN-VI,1375
27
+ fujin/secrets/__init__.py,sha256=WkdCtnBXjB7y_HHUWySf53LEe15Z7CZbE3cfDGxZiA8,1441
29
28
  fujin/secrets/bitwarden.py,sha256=01GZL5hYwZzL6yXy5ab3L3kgBFBeOT8i3Yg9GC8YwFU,2008
29
+ fujin/secrets/dopppler.py,sha256=t5SGfyuA0RxsD9uvrAu4cG2TBDIUgB5gb6XYXPrd61Y,724
30
30
  fujin/secrets/onepassword.py,sha256=6Xj3XWttKfcjMbcoMZvXVpJW1KHxlD785DysmX_mqvk,654
31
31
  fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
32
32
  fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
33
33
  fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
34
- fujin_cli-0.9.0.dist-info/METADATA,sha256=0OCkg_YffO4PLhTp1AKrk7OmncaCbZOO3IyTYkjGXxI,4576
35
- fujin_cli-0.9.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
- fujin_cli-0.9.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
37
- fujin_cli-0.9.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
38
- fujin_cli-0.9.0.dist-info/RECORD,,
34
+ fujin_cli-0.10.0.dist-info/METADATA,sha256=MBl9vT2C2Iq8pxVkuC9vgfmV7lhWBoZxjKxRf854QkQ,4577
35
+ fujin_cli-0.10.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
36
+ fujin_cli-0.10.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
37
+ fujin_cli-0.10.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
38
+ fujin_cli-0.10.0.dist-info/RECORD,,
@@ -1,40 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Protocol
4
- from typing import TYPE_CHECKING
5
-
6
- from fujin.connection import Connection
7
-
8
- if TYPE_CHECKING:
9
- from fujin.config import Config
10
-
11
-
12
- class ProcessManager(Protocol):
13
- service_names: list[str]
14
-
15
- @classmethod
16
- def create(cls, config: Config, conn: Connection) -> ProcessManager: ...
17
-
18
- def get_service_name(self, process_name: str): ...
19
-
20
- def install_services(self) -> None: ...
21
-
22
- def uninstall_services(self) -> None: ...
23
-
24
- def start_services(self, *names) -> None: ...
25
-
26
- def restart_services(self, *names) -> None: ...
27
-
28
- def stop_services(self, *names) -> None: ...
29
-
30
- def is_enabled(self, *names) -> dict[str, bool]: ...
31
-
32
- def is_active(self, *names) -> dict[str, bool]: ...
33
-
34
- def service_logs(self, name: str, follow: bool = False): ...
35
-
36
- def reload_configuration(self) -> None: ...
37
-
38
- def get_configuration_files(
39
- self, ignore_local: bool = False
40
- ) -> list[tuple[str, str]]: ...