fujin-cli 0.8.0__tar.gz → 0.9.1__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.

Potentially problematic release.


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

Files changed (81) hide show
  1. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/CHANGELOG.md +24 -0
  2. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/PKG-INFO +1 -1
  3. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/configuration.rst +2 -2
  4. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/pyproject.toml +2 -2
  5. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/_base.py +2 -12
  6. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/config.py +10 -2
  7. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/deploy.py +7 -7
  8. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/init.py +2 -2
  9. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/printenv.py +2 -2
  10. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/redeploy.py +2 -2
  11. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/config.py +28 -17
  12. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/proxies/caddy.py +2 -1
  13. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/secrets/__init__.py +13 -10
  14. {fujin_cli-0.8.0/src/fujin/process_managers → fujin_cli-0.9.1/src/fujin}/systemd.py +59 -24
  15. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/uv.lock +1 -1
  16. fujin_cli-0.8.0/src/fujin/process_managers/__init__.py +0 -40
  17. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/.github/workflows/publish.yml +0 -0
  18. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/.gitignore +0 -0
  19. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/.pre-commit-config.yaml +0 -0
  20. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/.readthedocs.yaml +0 -0
  21. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/LICENSE.txt +0 -0
  22. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/README.md +0 -0
  23. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/Vagrantfile +0 -0
  24. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/changelog.rst +0 -0
  25. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/app.rst +0 -0
  26. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/config.rst +0 -0
  27. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/deploy.rst +0 -0
  28. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/docs.rst +0 -0
  29. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/down.rst +0 -0
  30. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/index.rst +0 -0
  31. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/init.rst +0 -0
  32. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/printenv.rst +0 -0
  33. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/proxy.rst +0 -0
  34. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/prune.rst +0 -0
  35. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/redeploy.rst +0 -0
  36. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/rollback.rst +0 -0
  37. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/server.rst +0 -0
  38. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/commands/up.rst +0 -0
  39. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/conf.py +0 -0
  40. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/hooks.rst +0 -0
  41. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/index.rst +0 -0
  42. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/installation.rst +0 -0
  43. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/requirements.txt +0 -0
  44. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/secrets.rst +0 -0
  45. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/docs/tutorial.rst +0 -0
  46. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/README.md +0 -0
  47. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/__init__.py +0 -0
  48. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/__main__.py +0 -0
  49. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/asgi.py +0 -0
  50. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/settings.py +0 -0
  51. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/urls.py +0 -0
  52. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/bookstore/wsgi.py +0 -0
  53. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/fujin.toml +0 -0
  54. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/manage.py +0 -0
  55. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/pyproject.toml +0 -0
  56. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/django/bookstore/requirements.txt +0 -0
  57. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/golang/pocketbase/.env.prod +0 -0
  58. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/examples/golang/pocketbase/fujin.toml +0 -0
  59. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/justfile +0 -0
  60. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/__init__.py +0 -0
  61. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/__main__.py +0 -0
  62. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/__init__.py +0 -0
  63. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/app.py +0 -0
  64. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/docs.py +0 -0
  65. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/down.py +0 -0
  66. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/proxy.py +0 -0
  67. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/prune.py +0 -0
  68. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/rollback.py +0 -0
  69. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/server.py +0 -0
  70. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/commands/up.py +0 -0
  71. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/connection.py +0 -0
  72. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/errors.py +0 -0
  73. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/hooks.py +0 -0
  74. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/proxies/__init__.py +0 -0
  75. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/proxies/dummy.py +0 -0
  76. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/proxies/nginx.py +0 -0
  77. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/secrets/bitwarden.py +0 -0
  78. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/secrets/onepassword.py +0 -0
  79. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/templates/simple.service +0 -0
  80. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/templates/web.service +0 -0
  81. {fujin_cli-0.8.0 → fujin_cli-0.9.1}/src/fujin/templates/web.socket +0 -0
@@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.9.1] - 2024-11-23
8
+
9
+ ### 🚜 Refactor
10
+
11
+ - Drop configurable proxy manager
12
+
13
+ ### 📚 Documentation
14
+
15
+ - Add links to template systemd service files
16
+
17
+ ### ⚡ Performance
18
+
19
+ - Run systemd commands concurrently using gevent
20
+
21
+ ## [0.9.0] - 2024-11-23
22
+
23
+ ### 🚀 Features
24
+
25
+ - Env content can be define directly in toml
26
+
27
+ ### 🚜 Refactor
28
+
29
+ - Avoid running secret adapter if no secret placeholder is found
30
+
7
31
  ## [0.8.0] - 2024-11-23
8
32
 
9
33
  ### 🚀 Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: fujin-cli
3
- Version: 0.8.0
3
+ Version: 0.9.1
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
@@ -20,7 +20,7 @@ This is a minimal working example.
20
20
  from fujin.commands.init import simple_config
21
21
  from tomli_w import dumps
22
22
 
23
- print(dumps(simple_config("bookstore")))
23
+ print(dumps(simple_config("bookstore"), multiline_strings=True))
24
24
  #hide:toggle
25
25
 
26
26
  .. tab-item:: binary mode
@@ -32,5 +32,5 @@ This is a minimal working example.
32
32
  from fujin.commands.init import binary_config
33
33
  from tomli_w import dumps
34
34
 
35
- print(dumps(binary_config("bookstore")))
35
+ print(dumps(binary_config("bookstore"), multiline_strings=True))
36
36
  #hide:toggle
@@ -7,7 +7,7 @@ requires = [
7
7
 
8
8
  [project]
9
9
  name = "fujin-cli"
10
- version = "0.8.0"
10
+ version = "0.9.1"
11
11
  description = "Get your project up and running in a few minutes on your own vps."
12
12
  readme = "README.md"
13
13
  keywords = [
@@ -154,7 +154,7 @@ lint.isort.required-imports = [
154
154
  lint.pyupgrade.keep-runtime-typing = true
155
155
 
156
156
  [tool.bumpversion]
157
- current_version = "0.8.0"
157
+ current_version = "0.9.1"
158
158
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
159
159
  serialize = [
160
160
  "{major}.{minor}.{patch}",
@@ -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)
@@ -37,9 +37,17 @@ class ConfigCMD(BaseCommand):
37
37
  )
38
38
  )
39
39
 
40
+ host_config = {
41
+ f: getattr(self.config.host, f) for f in self.config.host.__struct_fields__
42
+ }
43
+ host_config.pop("_key_filename")
44
+ host_config.pop("_env_file")
45
+ host_config.pop("env_content")
46
+ if self.config.host.key_filename:
47
+ host_config["key_filename"] = self.config.host.key_filename
48
+ host_config["env_content"] = self.config.host.env_content
40
49
  host_config_text = "\n".join(
41
- f"[dim]{key}:[/dim] {value}"
42
- for key, value in self.config.host.to_dict().items()
50
+ f"[dim]{key}:[/dim] {value}" for key, value in host_config.items()
43
51
  )
44
52
  console.print(
45
53
  Panel(
@@ -52,15 +52,15 @@ class Deploy(BaseCommand):
52
52
  return f"{self.app_dir}/v{self.config.version}"
53
53
 
54
54
  def parse_envfile(self) -> str:
55
- if not self.config.host.envfile.exists():
56
- raise cappa.Exit(f"{self.config.host.envfile} not found", code=1)
57
55
  if self.config.secret_config:
58
56
  self.stdout.output("[blue]Reading secrets....[/blue]")
59
- return resolve_secrets(self.config.host.envfile, self.config.secret_config)
60
- return self.config.host.envfile.read_text()
57
+ return resolve_secrets(
58
+ self.config.host.env_content, self.config.secret_config
59
+ )
60
+ return self.config.host.env_content
61
61
 
62
62
  def transfer_files(
63
- self, conn: Connection, env: str, skip_requirements: bool = False
63
+ self, conn: Connection, env: str, skip_requirements: bool = False
64
64
  ):
65
65
  conn.run(f"echo '{env}' > {self.app_dir}/.env")
66
66
  distfile_path = self.config.get_distfile_path()
@@ -78,7 +78,7 @@ class Deploy(BaseCommand):
78
78
  )
79
79
 
80
80
  def install_project(
81
- self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
81
+ self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
82
82
  ):
83
83
  version = version or self.config.version
84
84
  if self.config.installation_mode == InstallationMode.PY_PACKAGE:
@@ -87,7 +87,7 @@ class Deploy(BaseCommand):
87
87
  self._install_binary(conn, version)
88
88
 
89
89
  def _install_python_package(
90
- self, conn: Connection, version: str, skip_setup: bool = False
90
+ self, conn: Connection, version: str, skip_setup: bool = False
91
91
  ):
92
92
  appenv = f"""
93
93
  set -a # Automatically export all variables
@@ -31,7 +31,7 @@ class Init(BaseCommand):
31
31
  }
32
32
  app_name = Path().resolve().stem.replace("-", "_").replace(" ", "_").lower()
33
33
  config = profile_to_func[self.profile](app_name)
34
- fujin_toml.write_text(tomli_w.dumps(config))
34
+ fujin_toml.write_text(tomli_w.dumps(config, multiline_strings=True))
35
35
  self.stdout.output(
36
36
  "[green]Sample configuration file generated successfully![/green]"
37
37
  )
@@ -57,7 +57,7 @@ def simple_config(app_name) -> dict:
57
57
  "host": {
58
58
  "user": "root",
59
59
  "domain_name": f"{app_name}.com",
60
- "envfile": ".env.prod",
60
+ "env_content": f"DEBUG=False\nALLOWED_HOSTS={app_name}.com\n",
61
61
  },
62
62
  }
63
63
  if not Path(".python-version").exists():
@@ -11,8 +11,8 @@ class Printenv(BaseCommand):
11
11
  def __call__(self):
12
12
  if self.config.secret_config:
13
13
  result = resolve_secrets(
14
- self.config.host.envfile, self.config.secret_config
14
+ self.config.host.env_content, self.config.secret_config
15
15
  )
16
16
  else:
17
- result = self.config.host.envfile.read_text()
17
+ result = self.config.host.env_content
18
18
  self.stdout.output(result)
@@ -34,8 +34,8 @@ class Redeploy(BaseCommand):
34
34
 
35
35
  def _copy_requirements_if_needed(self, conn: Connection) -> bool:
36
36
  if (
37
- not self.config.requirements
38
- or self.config.installation_mode == InstallationMode.BINARY
37
+ not self.config.requirements
38
+ or self.config.installation_mode == InstallationMode.BINARY
39
39
  ):
40
40
  return False
41
41
  local_requirements = hashlib.md5(
@@ -116,6 +116,11 @@ Example:
116
116
  .. note::
117
117
 
118
118
  Commands are relative to your ``app_dir``. When generating systemd service files, the full path is automatically constructed.
119
+ Here are the templates for the service files:
120
+
121
+ - `web.service <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/web.service>`_
122
+ - `web.socket <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/web.socket>`_
123
+ - `simple.service <https://github.com/falcopackages/fujin/blob/main/src/fujin/templates/simple.service>`_ (for all additional processes)
119
124
 
120
125
  Host Configuration
121
126
  -------------------
@@ -136,10 +141,18 @@ The login user for running remote tasks. Should have passwordless sudo access fo
136
141
 
137
142
  You can create a user with these requirements using the ``fujin server create-user`` command.
138
143
 
139
- envfile
140
- ~~~~~~~
144
+ env_file
145
+ ~~~~~~~~
141
146
  Path to the production environment file that will be copied to the host.
142
147
 
148
+ env_content
149
+ ~~~~~~~~~~~
150
+ A string containing the production environment variables, ideal for scenarios where most variables are retrieved from secrets and you prefer not to use a separate file.
151
+
152
+ .. important::
153
+
154
+ ``env_file`` and ``env_content`` are mutually exclusive—you can define only one.
155
+
143
156
  apps_dir
144
157
  ~~~~~~~~
145
158
 
@@ -178,7 +191,7 @@ Example:
178
191
 
179
192
  hooks
180
193
  -----
181
- Run custom scripts at specific points with hooks. Check out the `secrets </hooks.html>`_ page for more information.
194
+ Run custom scripts at specific points with hooks. Check out the `hooks </hooks.html>`_ page for more information.
182
195
 
183
196
  """
184
197
 
@@ -227,7 +240,6 @@ class Config(msgspec.Struct, kw_only=True):
227
240
  aliases: dict[str, str] = msgspec.field(default_factory=dict)
228
241
  host: HostConfig
229
242
  processes: dict[str, str] = msgspec.field(default_factory=dict)
230
- process_manager: str = "fujin.process_managers.systemd"
231
243
  webserver: Webserver
232
244
  requirements: str | None = None
233
245
  hooks: HooksDict = msgspec.field(default_factory=dict)
@@ -271,28 +283,27 @@ class HostConfig(msgspec.Struct, kw_only=True):
271
283
  ip: str | None = None
272
284
  domain_name: str
273
285
  user: str
274
- _envfile: str = msgspec.field(name="envfile")
286
+ _env_file: str = msgspec.field(name="envfile", default="")
287
+ env_content: str = ""
275
288
  apps_dir: str = ".local/share/fujin"
276
289
  password_env: str | None = None
277
290
  ssh_port: int = 22
278
291
  _key_filename: str | None = msgspec.field(name="key_filename", default=None)
279
292
 
280
293
  def __post_init__(self):
294
+ if self._env_file and self.env_content:
295
+ raise ImproperlyConfiguredError(
296
+ "Cannot set both 'env_content' and 'env_file' properties."
297
+ )
298
+ if not self.env_content:
299
+ envfile = Path(self._env_file)
300
+ if not envfile.exists():
301
+ raise ImproperlyConfiguredError(f"{self._env_file} not found")
302
+ self.env_content = envfile.read_text()
303
+ self.env_content = self.env_content.strip()
281
304
  self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
282
305
  self.ip = self.ip or self.domain_name
283
306
 
284
- def to_dict(self):
285
- d = {f: getattr(self, f) for f in self.__struct_fields__}
286
- d.pop("_key_filename")
287
- d.pop("_envfile")
288
- d["key_filename"] = self.key_filename
289
- d["envfile"] = self.envfile
290
- return d
291
-
292
- @property
293
- def envfile(self) -> Path:
294
- return Path(self._envfile)
295
-
296
307
  @property
297
308
  def key_filename(self) -> Path | None:
298
309
  if self._key_filename:
@@ -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:
@@ -1,19 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pathlib import Path
4
- from typing import Callable
3
+ from contextlib import closing
4
+ from io import StringIO
5
+ from typing import Callable, ContextManager
5
6
 
6
7
  import gevent
7
8
  from dotenv import dotenv_values
8
9
 
9
- from .bitwarden import bitwarden
10
- from .onepassword import one_password
11
10
  from fujin.config import SecretAdapter
12
11
  from fujin.config import SecretConfig
13
-
12
+ from .bitwarden import bitwarden
13
+ from .onepassword import one_password
14
14
 
15
15
  secret_reader = Callable[[str], str]
16
- secret_adapter_context = Callable[[SecretConfig], secret_reader]
16
+ secret_adapter_context = Callable[[SecretConfig], ContextManager[secret_reader]]
17
17
 
18
18
  adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
19
19
  SecretAdapter.BITWARDEN: bitwarden,
@@ -21,14 +21,17 @@ adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
21
21
  }
22
22
 
23
23
 
24
- def resolve_secrets(envfile: Path, secret_config: SecretConfig) -> str:
25
- env_dict = dotenv_values(envfile)
24
+ def resolve_secrets(env_content: str, secret_config: SecretConfig) -> str:
25
+ with closing(StringIO(env_content)) as buffer:
26
+ env_dict = dotenv_values(stream=buffer)
26
27
  secrets = {key: value for key, value in env_dict.items() if value.startswith("$")}
28
+ if not secrets:
29
+ return env_content
27
30
  adapter_context = adapter_to_context[secret_config.adapter]
28
31
  parsed_secrets = {}
29
- with adapter_context(secret_config) as secret_reader:
32
+ with adapter_context(secret_config) as reader:
30
33
  for key, secret in secrets.items():
31
- parsed_secrets[key] = gevent.spawn(secret_reader, secret[1:])
34
+ parsed_secrets[key] = gevent.spawn(reader, secret[1:])
32
35
  gevent.joinall(parsed_secrets.values())
33
36
  env_dict.update({key: thread.value for key, thread in parsed_secrets.items()})
34
37
  return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
@@ -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
@@ -477,7 +477,7 @@ wheels = [
477
477
 
478
478
  [[package]]
479
479
  name = "fujin-cli"
480
- version = "0.8.0"
480
+ version = "0.9.1"
481
481
  source = { editable = "." }
482
482
  dependencies = [
483
483
  { name = "cappa" },
@@ -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]]: ...
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