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

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.0}/CHANGELOG.md +10 -0
  2. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/PKG-INFO +1 -1
  3. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/configuration.rst +2 -2
  4. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/pyproject.toml +2 -2
  5. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/config.py +10 -2
  6. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/deploy.py +7 -7
  7. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/init.py +2 -2
  8. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/printenv.py +2 -2
  9. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/redeploy.py +2 -2
  10. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/config.py +23 -16
  11. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/__init__.py +13 -10
  12. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/uv.lock +1 -1
  13. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.github/workflows/publish.yml +0 -0
  14. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.gitignore +0 -0
  15. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.pre-commit-config.yaml +0 -0
  16. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.readthedocs.yaml +0 -0
  17. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/LICENSE.txt +0 -0
  18. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/README.md +0 -0
  19. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/Vagrantfile +0 -0
  20. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/changelog.rst +0 -0
  21. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/app.rst +0 -0
  22. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/config.rst +0 -0
  23. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/deploy.rst +0 -0
  24. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/docs.rst +0 -0
  25. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/down.rst +0 -0
  26. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/index.rst +0 -0
  27. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/init.rst +0 -0
  28. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/printenv.rst +0 -0
  29. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/proxy.rst +0 -0
  30. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/prune.rst +0 -0
  31. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/redeploy.rst +0 -0
  32. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/rollback.rst +0 -0
  33. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/server.rst +0 -0
  34. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/up.rst +0 -0
  35. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/conf.py +0 -0
  36. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/hooks.rst +0 -0
  37. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/index.rst +0 -0
  38. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/installation.rst +0 -0
  39. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/requirements.txt +0 -0
  40. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/secrets.rst +0 -0
  41. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/tutorial.rst +0 -0
  42. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/README.md +0 -0
  43. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
  44. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
  45. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
  46. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/settings.py +0 -0
  47. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/urls.py +0 -0
  48. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
  49. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/fujin.toml +0 -0
  50. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/manage.py +0 -0
  51. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/pyproject.toml +0 -0
  52. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/requirements.txt +0 -0
  53. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/golang/pocketbase/.env.prod +0 -0
  54. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/golang/pocketbase/fujin.toml +0 -0
  55. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/justfile +0 -0
  56. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/__init__.py +0 -0
  57. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/__main__.py +0 -0
  58. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/__init__.py +0 -0
  59. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/_base.py +0 -0
  60. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/app.py +0 -0
  61. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/docs.py +0 -0
  62. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/down.py +0 -0
  63. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/proxy.py +0 -0
  64. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/prune.py +0 -0
  65. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/rollback.py +0 -0
  66. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/server.py +0 -0
  67. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/up.py +0 -0
  68. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/connection.py +0 -0
  69. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/errors.py +0 -0
  70. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/hooks.py +0 -0
  71. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/process_managers/__init__.py +0 -0
  72. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/process_managers/systemd.py +0 -0
  73. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/__init__.py +0 -0
  74. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/caddy.py +0 -0
  75. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/dummy.py +0 -0
  76. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/nginx.py +0 -0
  77. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/bitwarden.py +0 -0
  78. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/onepassword.py +0 -0
  79. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/simple.service +0 -0
  80. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/web.service +0 -0
  81. {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/web.socket +0 -0
@@ -4,6 +4,16 @@ 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.0] - 2024-11-23
8
+
9
+ ### 🚀 Features
10
+
11
+ - Env content can be define directly in toml
12
+
13
+ ### 🚜 Refactor
14
+
15
+ - Avoid running secret adapter if no secret placeholder is found
16
+
7
17
  ## [0.8.0] - 2024-11-23
8
18
 
9
19
  ### 🚀 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.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
@@ -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.0"
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.0"
158
158
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
159
159
  serialize = [
160
160
  "{major}.{minor}.{patch}",
@@ -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(
@@ -136,10 +136,18 @@ The login user for running remote tasks. Should have passwordless sudo access fo
136
136
 
137
137
  You can create a user with these requirements using the ``fujin server create-user`` command.
138
138
 
139
- envfile
140
- ~~~~~~~
139
+ env_file
140
+ ~~~~~~~~
141
141
  Path to the production environment file that will be copied to the host.
142
142
 
143
+ env_content
144
+ ~~~~~~~~~~~
145
+ 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.
146
+
147
+ .. important::
148
+
149
+ ``env_file`` and ``env_content`` are mutually exclusive—you can define only one.
150
+
143
151
  apps_dir
144
152
  ~~~~~~~~
145
153
 
@@ -178,7 +186,7 @@ Example:
178
186
 
179
187
  hooks
180
188
  -----
181
- Run custom scripts at specific points with hooks. Check out the `secrets </hooks.html>`_ page for more information.
189
+ Run custom scripts at specific points with hooks. Check out the `hooks </hooks.html>`_ page for more information.
182
190
 
183
191
  """
184
192
 
@@ -271,28 +279,27 @@ class HostConfig(msgspec.Struct, kw_only=True):
271
279
  ip: str | None = None
272
280
  domain_name: str
273
281
  user: str
274
- _envfile: str = msgspec.field(name="envfile")
282
+ _env_file: str = msgspec.field(name="envfile", default="")
283
+ env_content: str = ""
275
284
  apps_dir: str = ".local/share/fujin"
276
285
  password_env: str | None = None
277
286
  ssh_port: int = 22
278
287
  _key_filename: str | None = msgspec.field(name="key_filename", default=None)
279
288
 
280
289
  def __post_init__(self):
290
+ if self._env_file and self.env_content:
291
+ raise ImproperlyConfiguredError(
292
+ "Cannot set both 'env_content' and 'env_file' properties."
293
+ )
294
+ if not self.env_content:
295
+ envfile = Path(self._env_file)
296
+ if not envfile.exists():
297
+ raise ImproperlyConfiguredError(f"{self._env_file} not found")
298
+ self.env_content = envfile.read_text()
299
+ self.env_content = self.env_content.strip()
281
300
  self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
282
301
  self.ip = self.ip or self.domain_name
283
302
 
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
303
  @property
297
304
  def key_filename(self) -> Path | None:
298
305
  if self._key_filename:
@@ -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())
@@ -477,7 +477,7 @@ wheels = [
477
477
 
478
478
  [[package]]
479
479
  name = "fujin-cli"
480
- version = "0.8.0"
480
+ version = "0.9.0"
481
481
  source = { editable = "." }
482
482
  dependencies = [
483
483
  { name = "cappa" },
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