fujin-cli 0.4.0__py3-none-any.whl → 0.6.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/app.py +3 -1
- fujin/commands/config.py +3 -2
- fujin/commands/deploy.py +51 -29
- fujin/commands/down.py +1 -1
- fujin/commands/init.py +52 -17
- fujin/commands/proxy.py +11 -0
- fujin/commands/redeploy.py +38 -25
- fujin/commands/server.py +0 -5
- fujin/config.py +36 -24
- fujin/proxies/caddy.py +61 -47
- fujin/proxies/nginx.py +14 -16
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.6.0.dist-info}/METADATA +8 -5
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.6.0.dist-info}/RECORD +16 -16
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.6.0.dist-info}/WHEEL +1 -1
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.6.0.dist-info}/entry_points.txt +0 -0
- {fujin_cli-0.4.0.dist-info → fujin_cli-0.6.0.dist-info}/licenses/LICENSE.txt +0 -0
fujin/commands/app.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Annotated
|
|
|
5
5
|
import cappa
|
|
6
6
|
|
|
7
7
|
from fujin.commands import BaseCommand
|
|
8
|
+
from fujin.config import InstallationMode
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@cappa.command(help="Run application-related tasks")
|
|
@@ -25,11 +26,12 @@ class App(BaseCommand):
|
|
|
25
26
|
"app_bin": self.config.app_bin,
|
|
26
27
|
"local_version": self.config.version,
|
|
27
28
|
"remote_version": remote_version,
|
|
28
|
-
"python_version": self.config.python_version,
|
|
29
29
|
"rollback_targets": ", ".join(rollback_targets.split("\n"))
|
|
30
30
|
if rollback_targets
|
|
31
31
|
else "N/A",
|
|
32
32
|
}
|
|
33
|
+
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
34
|
+
infos["python_version"] = self.config.python_version
|
|
33
35
|
pm = self.create_process_manager(conn)
|
|
34
36
|
services: dict[str, bool] = pm.is_active()
|
|
35
37
|
|
fujin/commands/config.py
CHANGED
|
@@ -16,13 +16,14 @@ class ConfigCMD(BaseCommand):
|
|
|
16
16
|
"app": self.config.app_name,
|
|
17
17
|
"app_bin": self.config.app_bin,
|
|
18
18
|
"version": self.config.version,
|
|
19
|
-
"python_version": self.config.python_version,
|
|
20
19
|
"build_command": self.config.build_command,
|
|
21
20
|
"release_command": self.config.release_command,
|
|
21
|
+
"installation_mode": self.config.installation_mode,
|
|
22
22
|
"distfile": self.config.distfile,
|
|
23
|
-
"requirements": self.config.requirements,
|
|
24
23
|
"webserver": f"{{ upstream = '{self.config.webserver.upstream}', type = '{self.config.webserver.type}' }}",
|
|
25
24
|
}
|
|
25
|
+
if self.config.python_version:
|
|
26
|
+
general_config["python_version"] = self.config.python_version
|
|
26
27
|
general_config_text = "\n".join(
|
|
27
28
|
f"[bold green]{key}:[/bold green] {value}"
|
|
28
29
|
for key, value in general_config.items()
|
fujin/commands/deploy.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import cappa
|
|
6
7
|
|
|
7
8
|
from fujin.commands import BaseCommand
|
|
9
|
+
from fujin.config import InstallationMode
|
|
8
10
|
from fujin.connection import Connection
|
|
9
11
|
|
|
10
12
|
|
|
@@ -16,21 +18,20 @@ class Deploy(BaseCommand):
|
|
|
16
18
|
self.build_app()
|
|
17
19
|
|
|
18
20
|
with self.connection() as conn:
|
|
21
|
+
process_manager = self.create_process_manager(conn)
|
|
19
22
|
conn.run(f"mkdir -p {self.app_dir}")
|
|
20
23
|
with conn.cd(self.app_dir):
|
|
21
24
|
self.create_hook_manager(conn).pre_deploy()
|
|
22
25
|
self.transfer_files(conn)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
self.update_version_history(conn)
|
|
33
|
-
self.prune_assets(conn)
|
|
26
|
+
self.install_project(conn)
|
|
27
|
+
with self.app_environment() as app_conn:
|
|
28
|
+
self.release(app_conn)
|
|
29
|
+
process_manager.install_services()
|
|
30
|
+
process_manager.reload_configuration()
|
|
31
|
+
process_manager.restart_services()
|
|
32
|
+
self.create_web_proxy(app_conn).setup()
|
|
33
|
+
self.update_version_history(app_conn)
|
|
34
|
+
self.prune_assets(app_conn)
|
|
34
35
|
self.create_hook_manager(conn).post_deploy()
|
|
35
36
|
self.stdout.output("[green]Project deployment completed successfully![/green]")
|
|
36
37
|
self.stdout.output(
|
|
@@ -47,24 +48,37 @@ class Deploy(BaseCommand):
|
|
|
47
48
|
def versioned_assets_dir(self) -> str:
|
|
48
49
|
return f"{self.app_dir}/v{self.config.version}"
|
|
49
50
|
|
|
50
|
-
def transfer_files(self, conn: Connection
|
|
51
|
+
def transfer_files(self, conn: Connection):
|
|
51
52
|
if not self.config.host.envfile.exists():
|
|
52
53
|
raise cappa.Exit(f"{self.config.host.envfile} not found", code=1)
|
|
53
|
-
|
|
54
|
-
if not self.config.requirements.exists():
|
|
55
|
-
raise cappa.Exit(f"{self.config.requirements} not found", code=1)
|
|
56
54
|
conn.put(str(self.config.host.envfile), f"{self.app_dir}/.env")
|
|
57
55
|
conn.run(f"mkdir -p {self.versioned_assets_dir}")
|
|
58
|
-
if not skip_requirements:
|
|
59
|
-
conn.put(
|
|
60
|
-
str(self.config.requirements),
|
|
61
|
-
f"{self.versioned_assets_dir}/requirements.txt",
|
|
62
|
-
)
|
|
63
56
|
distfile_path = self.config.get_distfile_path()
|
|
64
57
|
conn.put(
|
|
65
58
|
str(distfile_path),
|
|
66
59
|
f"{self.versioned_assets_dir}/{distfile_path.name}",
|
|
67
60
|
)
|
|
61
|
+
|
|
62
|
+
def install_project(
|
|
63
|
+
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
64
|
+
):
|
|
65
|
+
version = version or self.config.version
|
|
66
|
+
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
67
|
+
self._install_python_package(conn, version, skip_setup)
|
|
68
|
+
else:
|
|
69
|
+
self._install_binary(conn, version)
|
|
70
|
+
|
|
71
|
+
def _install_python_package(
|
|
72
|
+
self, conn: Connection, version: str, skip_setup: bool = False
|
|
73
|
+
):
|
|
74
|
+
if not skip_setup and self.config.requirements:
|
|
75
|
+
requirements = Path(self.config.requirements)
|
|
76
|
+
if not requirements.exists():
|
|
77
|
+
raise cappa.Exit(f"{self.config.requirements} not found", code=1)
|
|
78
|
+
conn.put(
|
|
79
|
+
Path(self.config.requirements).resolve(),
|
|
80
|
+
f"{self.versioned_assets_dir}/requirements.txt",
|
|
81
|
+
)
|
|
68
82
|
appenv = f"""
|
|
69
83
|
set -a # Automatically export all variables
|
|
70
84
|
source .env
|
|
@@ -73,22 +87,30 @@ export UV_COMPILE_BYTECODE=1
|
|
|
73
87
|
export UV_PYTHON=python{self.config.python_version}
|
|
74
88
|
export PATH=".venv/bin:$PATH"
|
|
75
89
|
"""
|
|
76
|
-
conn.run(f"echo '{appenv.strip()}' > .appenv")
|
|
77
|
-
|
|
78
|
-
def install_project(
|
|
79
|
-
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
80
|
-
):
|
|
81
|
-
if self.config.skip_project_install:
|
|
82
|
-
return
|
|
83
|
-
version = version or self.config.version
|
|
90
|
+
conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
|
|
84
91
|
versioned_assets_dir = f"{self.app_dir}/v{version}"
|
|
85
92
|
if not skip_setup:
|
|
86
93
|
conn.run("uv venv")
|
|
87
|
-
|
|
94
|
+
if self.config.requirements:
|
|
95
|
+
conn.run(f"uv pip install -r {versioned_assets_dir}/requirements.txt")
|
|
88
96
|
conn.run(
|
|
89
97
|
f"uv pip install {versioned_assets_dir}/{self.config.get_distfile_path(version).name}"
|
|
90
98
|
)
|
|
91
99
|
|
|
100
|
+
def _install_binary(self, conn: Connection, version: str):
|
|
101
|
+
appenv = f"""
|
|
102
|
+
set -a # Automatically export all variables
|
|
103
|
+
source .env
|
|
104
|
+
set +a # Stop automatic export
|
|
105
|
+
export PATH="{self.app_dir}:$PATH"
|
|
106
|
+
"""
|
|
107
|
+
conn.run(f"echo '{appenv.strip()}' > {self.app_dir}/.appenv")
|
|
108
|
+
full_path_app_bin = f"{self.app_dir}/{self.config.app_bin}"
|
|
109
|
+
conn.run(f"rm {full_path_app_bin}", warn=True)
|
|
110
|
+
conn.run(
|
|
111
|
+
f"ln -s {self.versioned_assets_dir}/{self.config.get_distfile_path(version).name} {full_path_app_bin}"
|
|
112
|
+
)
|
|
113
|
+
|
|
92
114
|
def release(self, conn: Connection):
|
|
93
115
|
if self.config.release_command:
|
|
94
116
|
conn.run(f"source .env && {self.config.release_command}")
|
fujin/commands/down.py
CHANGED
|
@@ -28,7 +28,7 @@ class Down(BaseCommand):
|
|
|
28
28
|
confirm = Confirm.ask(
|
|
29
29
|
f"""[red]You are about to delete all project files, stop all services, and remove all configurations on the host {self.config.host.ip} for the project {self.config.app_name}. Any assets in your project folder will be lost (sqlite not in there ?). Are you sure you want to proceed? This action is irreversible.[/red]""",
|
|
30
30
|
)
|
|
31
|
-
except KeyboardInterrupt
|
|
31
|
+
except KeyboardInterrupt:
|
|
32
32
|
raise cappa.Exit("Teardown aborted", code=0)
|
|
33
33
|
if not confirm:
|
|
34
34
|
return
|
fujin/commands/init.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Annotated
|
|
5
6
|
|
|
@@ -7,31 +8,28 @@ import cappa
|
|
|
7
8
|
import tomli_w
|
|
8
9
|
|
|
9
10
|
from fujin.commands import BaseCommand
|
|
10
|
-
from fujin.config import tomllib
|
|
11
|
+
from fujin.config import tomllib, InstallationMode
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
@cappa.command(help="Generate a sample configuration file")
|
|
15
|
+
@dataclass
|
|
14
16
|
class Init(BaseCommand):
|
|
15
17
|
profile: Annotated[
|
|
16
|
-
str,
|
|
18
|
+
str,
|
|
19
|
+
cappa.Arg(choices=["simple", "falco", "binary"], short="-p", long="--profile"),
|
|
17
20
|
] = "simple"
|
|
18
21
|
|
|
19
22
|
def __call__(self):
|
|
20
23
|
fujin_toml = Path("fujin.toml")
|
|
21
24
|
if fujin_toml.exists():
|
|
22
25
|
raise cappa.Exit("fujin.toml file already exists", code=1)
|
|
23
|
-
profile_to_func = {
|
|
26
|
+
profile_to_func = {
|
|
27
|
+
"simple": simple_config,
|
|
28
|
+
"falco": falco_config,
|
|
29
|
+
"binary": binary_config,
|
|
30
|
+
}
|
|
24
31
|
app_name = Path().resolve().stem.replace("-", "_").replace(" ", "_").lower()
|
|
25
32
|
config = profile_to_func[self.profile](app_name)
|
|
26
|
-
if not Path(".python-version").exists():
|
|
27
|
-
config["python_version"] = "3.12"
|
|
28
|
-
pyproject_toml = Path("pyproject.toml")
|
|
29
|
-
if pyproject_toml.exists():
|
|
30
|
-
pyproject = tomllib.loads(pyproject_toml.read_text())
|
|
31
|
-
config["app"] = pyproject.get("project", {}).get("name", app_name)
|
|
32
|
-
if pyproject.get("project", {}).get("version"):
|
|
33
|
-
# fujin will read the version itself from the pyproject
|
|
34
|
-
config.pop("version")
|
|
35
33
|
fujin_toml.write_text(tomli_w.dumps(config))
|
|
36
34
|
self.stdout.output(
|
|
37
35
|
"[green]Sample configuration file generated successfully![/green]"
|
|
@@ -39,27 +37,38 @@ class Init(BaseCommand):
|
|
|
39
37
|
|
|
40
38
|
|
|
41
39
|
def simple_config(app_name) -> dict:
|
|
42
|
-
|
|
40
|
+
config = {
|
|
43
41
|
"app": app_name,
|
|
44
|
-
"version": "0.1
|
|
42
|
+
"version": "0.0.1",
|
|
45
43
|
"build_command": "uv build && uv pip compile pyproject.toml -o requirements.txt",
|
|
46
44
|
"distfile": f"dist/{app_name}-{{version}}-py3-none-any.whl",
|
|
45
|
+
"requirements": "requirements.txt",
|
|
47
46
|
"webserver": {
|
|
48
|
-
"upstream": "
|
|
47
|
+
"upstream": f"unix//run/{app_name}.sock",
|
|
49
48
|
"type": "fujin.proxies.caddy",
|
|
50
49
|
},
|
|
51
50
|
"release_command": f"{app_name} migrate",
|
|
51
|
+
"installation_mode": InstallationMode.PY_PACKAGE,
|
|
52
52
|
"processes": {
|
|
53
|
-
"web": f".venv/bin/gunicorn {app_name}.wsgi:application --bind
|
|
53
|
+
"web": f".venv/bin/gunicorn {app_name}.wsgi:application --bind unix//run/{app_name}.sock"
|
|
54
54
|
},
|
|
55
55
|
"aliases": {"shell": "server exec --appenv -i bash"},
|
|
56
56
|
"host": {
|
|
57
|
-
"ip": "127.0.0.1",
|
|
58
57
|
"user": "root",
|
|
59
58
|
"domain_name": f"{app_name}.com",
|
|
60
59
|
"envfile": ".env.prod",
|
|
61
60
|
},
|
|
62
61
|
}
|
|
62
|
+
if not Path(".python-version").exists():
|
|
63
|
+
config["python_version"] = "3.12"
|
|
64
|
+
pyproject_toml = Path("pyproject.toml")
|
|
65
|
+
if pyproject_toml.exists():
|
|
66
|
+
pyproject = tomllib.loads(pyproject_toml.read_text())
|
|
67
|
+
config["app"] = pyproject.get("project", {}).get("name", app_name)
|
|
68
|
+
if pyproject.get("project", {}).get("version"):
|
|
69
|
+
# fujin will read the version itself from the pyproject
|
|
70
|
+
config.pop("version")
|
|
71
|
+
return config
|
|
63
72
|
|
|
64
73
|
|
|
65
74
|
def falco_config(app_name: str) -> dict:
|
|
@@ -71,6 +80,10 @@ def falco_config(app_name: str) -> dict:
|
|
|
71
80
|
"web": f".venv/bin/{config['app']} prodserver",
|
|
72
81
|
"worker": f".venv/bin/{config['app']} qcluster",
|
|
73
82
|
},
|
|
83
|
+
"webserver": {
|
|
84
|
+
"upstream": "localhost:8000",
|
|
85
|
+
"type": "fujin.proxies.caddy",
|
|
86
|
+
},
|
|
74
87
|
"aliases": {
|
|
75
88
|
"console": "app exec -i shell_plus",
|
|
76
89
|
"dbconsole": "app exec -i dbshell",
|
|
@@ -80,3 +93,25 @@ def falco_config(app_name: str) -> dict:
|
|
|
80
93
|
}
|
|
81
94
|
)
|
|
82
95
|
return config
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def binary_config(app_name: str) -> dict:
|
|
99
|
+
return {
|
|
100
|
+
"app": app_name,
|
|
101
|
+
"version": "0.0.1",
|
|
102
|
+
"build_command": "just build-bin",
|
|
103
|
+
"distfile": f"dist/bin/{app_name}-{{version}}",
|
|
104
|
+
"webserver": {
|
|
105
|
+
"upstream": "localhost:8000",
|
|
106
|
+
"type": "fujin.proxies.caddy",
|
|
107
|
+
},
|
|
108
|
+
"release_command": f"{app_name} migrate",
|
|
109
|
+
"installation_mode": InstallationMode.BINARY,
|
|
110
|
+
"processes": {"web": f"{app_name} prodserver"},
|
|
111
|
+
"aliases": {"shell": "server exec --appenv -i bash"},
|
|
112
|
+
"host": {
|
|
113
|
+
"user": "root",
|
|
114
|
+
"domain_name": f"{app_name}.com",
|
|
115
|
+
"envfile": ".env.prod",
|
|
116
|
+
},
|
|
117
|
+
}
|
fujin/commands/proxy.py
CHANGED
|
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
|
|
2
2
|
from typing import Annotated
|
|
3
3
|
|
|
4
4
|
import cappa
|
|
5
|
+
from rich.prompt import Confirm
|
|
5
6
|
|
|
6
7
|
from fujin.commands import BaseCommand
|
|
7
8
|
|
|
@@ -13,11 +14,21 @@ class Proxy(BaseCommand):
|
|
|
13
14
|
def install(self):
|
|
14
15
|
with self.connection() as conn:
|
|
15
16
|
self.create_web_proxy(conn).install()
|
|
17
|
+
self.stdout.output("[green]Proxy installed successfully![/green]")
|
|
16
18
|
|
|
17
19
|
@cappa.command(help="Uninstall the proxy from the remote host")
|
|
18
20
|
def uninstall(self):
|
|
21
|
+
try:
|
|
22
|
+
confirm = Confirm.ask(
|
|
23
|
+
f"[red]Uninstalling the proxy will remove all current configurations. Are you sure you want to proceed?"
|
|
24
|
+
)
|
|
25
|
+
except KeyboardInterrupt:
|
|
26
|
+
raise cappa.Exit("Teardown aborted", code=0)
|
|
27
|
+
if not confirm:
|
|
28
|
+
return
|
|
19
29
|
with self.connection() as conn:
|
|
20
30
|
self.create_web_proxy(conn).uninstall()
|
|
31
|
+
self.stdout.output("[green]Proxy uninstalled successfully![/green]")
|
|
21
32
|
|
|
22
33
|
@cappa.command(help="Start the proxy on the remote host")
|
|
23
34
|
def start(self):
|
fujin/commands/redeploy.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import cappa
|
|
6
7
|
|
|
7
8
|
from fujin.commands import BaseCommand
|
|
8
9
|
from .deploy import Deploy
|
|
10
|
+
from fujin.config import InstallationMode
|
|
11
|
+
from fujin.connection import Connection
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@cappa.command(help="Redeploy the application to apply code and environment changes")
|
|
@@ -13,36 +16,46 @@ class Redeploy(BaseCommand):
|
|
|
13
16
|
def __call__(self):
|
|
14
17
|
deploy = Deploy()
|
|
15
18
|
deploy.build_app()
|
|
16
|
-
|
|
17
|
-
self.config.requirements.read_bytes()
|
|
18
|
-
).hexdigest()
|
|
19
|
+
|
|
19
20
|
with self.app_environment() as conn:
|
|
20
21
|
hook_manager = self.create_hook_manager(conn)
|
|
21
22
|
hook_manager.pre_deploy()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
try:
|
|
26
|
-
host_requirements = (
|
|
27
|
-
conn.run(
|
|
28
|
-
f"md5sum v{current_host_version}/requirements.txt",
|
|
29
|
-
warn=True,
|
|
30
|
-
hide=True,
|
|
31
|
-
)
|
|
32
|
-
.stdout.strip()
|
|
33
|
-
.split()[0]
|
|
34
|
-
)
|
|
35
|
-
skip_requirements = host_requirements == local_requirements
|
|
36
|
-
except IndexError:
|
|
37
|
-
skip_requirements = False
|
|
38
|
-
deploy.transfer_files(conn, skip_requirements=skip_requirements)
|
|
39
|
-
if skip_requirements and current_host_version != self.config.version:
|
|
40
|
-
conn.run(
|
|
41
|
-
f"cp v{current_host_version}/requirements.txt {deploy.versioned_assets_dir}/requirements.txt "
|
|
42
|
-
)
|
|
43
|
-
deploy.install_project(conn, skip_setup=skip_requirements)
|
|
23
|
+
deploy.transfer_files(conn)
|
|
24
|
+
requirements_copied = self._copy_requirements_if_needed(conn)
|
|
25
|
+
deploy.install_project(conn, skip_setup=requirements_copied)
|
|
44
26
|
deploy.release(conn)
|
|
45
27
|
self.create_process_manager(conn).restart_services()
|
|
46
28
|
deploy.update_version_history(conn)
|
|
47
29
|
hook_manager.post_deploy()
|
|
48
30
|
self.stdout.output("[green]Redeployment completed successfully![/green]")
|
|
31
|
+
|
|
32
|
+
def _copy_requirements_if_needed(self, conn: Connection) -> bool:
|
|
33
|
+
if (
|
|
34
|
+
not self.config.requirements
|
|
35
|
+
or self.config.installation_mode == InstallationMode.BINARY
|
|
36
|
+
):
|
|
37
|
+
return False
|
|
38
|
+
local_requirements = hashlib.md5(
|
|
39
|
+
Path(self.config.requirements).read_bytes()
|
|
40
|
+
).hexdigest()
|
|
41
|
+
current_host_version = conn.run(
|
|
42
|
+
"head -n 1 .versions", warn=True, hide=True
|
|
43
|
+
).stdout.strip()
|
|
44
|
+
try:
|
|
45
|
+
host_requirements = (
|
|
46
|
+
conn.run(
|
|
47
|
+
f"md5sum v{current_host_version}/requirements.txt",
|
|
48
|
+
warn=True,
|
|
49
|
+
hide=True,
|
|
50
|
+
)
|
|
51
|
+
.stdout.strip()
|
|
52
|
+
.split()[0]
|
|
53
|
+
)
|
|
54
|
+
skip_requirements = host_requirements == local_requirements
|
|
55
|
+
except IndexError:
|
|
56
|
+
return False
|
|
57
|
+
if skip_requirements and current_host_version != self.config.version:
|
|
58
|
+
conn.run(
|
|
59
|
+
f"cp v{current_host_version}/requirements.txt {self.app_dir}/v{self.config.version}/requirements.txt "
|
|
60
|
+
)
|
|
61
|
+
return True
|
fujin/commands/server.py
CHANGED
|
@@ -38,11 +38,6 @@ class Server(BaseCommand):
|
|
|
38
38
|
"[green]Server bootstrap completed successfully![/green]"
|
|
39
39
|
)
|
|
40
40
|
|
|
41
|
-
@cappa.command(help="Stop and uninstall the web proxy")
|
|
42
|
-
def uninstall_proxy(self):
|
|
43
|
-
with self.connection() as conn:
|
|
44
|
-
self.create_web_proxy(conn).uninstall()
|
|
45
|
-
|
|
46
41
|
@cappa.command(
|
|
47
42
|
help="Execute an arbitrary command on the server, optionally in interactive mode"
|
|
48
43
|
)
|
fujin/config.py
CHANGED
|
@@ -5,18 +5,18 @@ app
|
|
|
5
5
|
---
|
|
6
6
|
The name of your project or application. Must be a valid Python package name.
|
|
7
7
|
|
|
8
|
-
app_bin
|
|
9
|
-
-------
|
|
10
|
-
Path to your application's executable. Used by the **app** subcommand for remote execution.
|
|
11
|
-
Default: ``.venv/bin/{app}``
|
|
12
|
-
|
|
13
8
|
version
|
|
14
9
|
--------
|
|
15
10
|
The version of your project to build and deploy. If not specified, automatically parsed from ``pyproject.toml`` under ``project.version``.
|
|
16
11
|
|
|
17
12
|
python_version
|
|
18
13
|
--------------
|
|
19
|
-
The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file.
|
|
14
|
+
The Python version for your virtualenv. If not specified, automatically parsed from ``.python-version`` file. This is only
|
|
15
|
+
required if the installation mode is set to ``python-package``
|
|
16
|
+
|
|
17
|
+
requirements
|
|
18
|
+
------------
|
|
19
|
+
Optional path to your requirements file. This will only be used when the installation mode is set to ``python-package``
|
|
20
20
|
|
|
21
21
|
versions_to_keep
|
|
22
22
|
----------------
|
|
@@ -32,15 +32,17 @@ distfile
|
|
|
32
32
|
Path to your project's distribution file. This should be the main artifact containing everything needed to run your project on the server.
|
|
33
33
|
Supports version placeholder, e.g., ``dist/app_name-{version}-py3-none-any.whl``
|
|
34
34
|
|
|
35
|
+
installation_mode
|
|
36
|
+
-----------------
|
|
37
|
+
|
|
38
|
+
Indicates whether the ``distfile`` is a Python package or a self-contained executable. The possible values are ``python-package`` and ``binary``.
|
|
39
|
+
The ``binary`` option disables specific Python-related features, such as virtual environment creation and requirements installation. ``fujin`` will assume the provided
|
|
40
|
+
``distfile`` already contains all the necessary dependencies to run your program.
|
|
41
|
+
|
|
35
42
|
release_command
|
|
36
43
|
---------------
|
|
37
44
|
Optional command to run at the end of deployment (e.g., database migrations).
|
|
38
45
|
|
|
39
|
-
requirements
|
|
40
|
-
------------
|
|
41
|
-
Path to your requirements file.
|
|
42
|
-
Default: ``requirements.txt``
|
|
43
|
-
|
|
44
46
|
Webserver
|
|
45
47
|
---------
|
|
46
48
|
|
|
@@ -63,7 +65,8 @@ statics
|
|
|
63
65
|
~~~~~~~
|
|
64
66
|
|
|
65
67
|
Defines the mapping of URL paths to local directories for serving static files. The syntax and support for static
|
|
66
|
-
file serving depend on the selected reverse proxy.
|
|
68
|
+
file serving depend on the selected reverse proxy. The directories you map should be accessible by the web server, meaning
|
|
69
|
+
with read permissions for the ``www-data`` group; a reliable choice is ``/var/www``.
|
|
67
70
|
|
|
68
71
|
Example:
|
|
69
72
|
|
|
@@ -72,7 +75,7 @@ Example:
|
|
|
72
75
|
[webserver]
|
|
73
76
|
upstream = "unix//run/project.sock"
|
|
74
77
|
type = "fujin.proxies.caddy"
|
|
75
|
-
statics = { "/static/*" = "/var/www/
|
|
78
|
+
statics = { "/static/*" = "/var/www/myproject/static/" }
|
|
76
79
|
|
|
77
80
|
processes
|
|
78
81
|
---------
|
|
@@ -98,7 +101,7 @@ Host Configuration
|
|
|
98
101
|
|
|
99
102
|
ip
|
|
100
103
|
~~
|
|
101
|
-
The IP address or
|
|
104
|
+
The IP address or anything that resolves to the remote host IP's. This is use to communicate via ssh with the server, if omitted it's value will default to the one of the ``domain_name``.
|
|
102
105
|
|
|
103
106
|
domain_name
|
|
104
107
|
~~~~~~~~~~~
|
|
@@ -159,6 +162,7 @@ from __future__ import annotations
|
|
|
159
162
|
|
|
160
163
|
import os
|
|
161
164
|
import sys
|
|
165
|
+
from functools import cached_property
|
|
162
166
|
from pathlib import Path
|
|
163
167
|
|
|
164
168
|
import msgspec
|
|
@@ -170,31 +174,36 @@ if sys.version_info >= (3, 11):
|
|
|
170
174
|
else:
|
|
171
175
|
import tomli as tomllib
|
|
172
176
|
|
|
173
|
-
from .hooks import HooksDict
|
|
177
|
+
from .hooks import HooksDict, StrEnum
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class InstallationMode(StrEnum):
|
|
181
|
+
PY_PACKAGE = "python-package"
|
|
182
|
+
BINARY = "binary"
|
|
174
183
|
|
|
175
184
|
|
|
176
185
|
class Config(msgspec.Struct, kw_only=True):
|
|
177
186
|
app_name: str = msgspec.field(name="app")
|
|
178
|
-
app_bin: str = ".venv/bin/{app}"
|
|
179
187
|
version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
|
|
180
188
|
versions_to_keep: int | None = 5
|
|
181
|
-
python_version: str
|
|
189
|
+
python_version: str | None = None
|
|
182
190
|
build_command: str
|
|
183
191
|
release_command: str | None = None
|
|
184
|
-
|
|
192
|
+
installation_mode: InstallationMode
|
|
185
193
|
distfile: str
|
|
186
194
|
aliases: dict[str, str] = msgspec.field(default_factory=dict)
|
|
187
195
|
host: HostConfig
|
|
188
196
|
processes: dict[str, str] = msgspec.field(default_factory=dict)
|
|
189
197
|
process_manager: str = "fujin.process_managers.systemd"
|
|
190
198
|
webserver: Webserver
|
|
191
|
-
|
|
199
|
+
requirements: str | None = None
|
|
192
200
|
hooks: HooksDict = msgspec.field(default_factory=dict)
|
|
193
201
|
local_config_dir: Path = Path(".fujin")
|
|
194
202
|
|
|
195
203
|
def __post_init__(self):
|
|
196
|
-
self.
|
|
197
|
-
|
|
204
|
+
if self.installation_mode == InstallationMode.PY_PACKAGE:
|
|
205
|
+
if not self.python_version:
|
|
206
|
+
self.python_version = find_python_version()
|
|
198
207
|
|
|
199
208
|
if "web" not in self.processes and self.webserver.type != "fujin.proxies.dummy":
|
|
200
209
|
raise ValueError(
|
|
@@ -202,8 +211,10 @@ class Config(msgspec.Struct, kw_only=True):
|
|
|
202
211
|
)
|
|
203
212
|
|
|
204
213
|
@property
|
|
205
|
-
def
|
|
206
|
-
|
|
214
|
+
def app_bin(self) -> str:
|
|
215
|
+
if self.installation_mode == InstallationMode.PY_PACKAGE:
|
|
216
|
+
return f".venv/bin/{self.app_name}"
|
|
217
|
+
return self.app_name
|
|
207
218
|
|
|
208
219
|
def get_distfile_path(self, version: str | None = None) -> Path:
|
|
209
220
|
version = version or self.version
|
|
@@ -223,7 +234,7 @@ class Config(msgspec.Struct, kw_only=True):
|
|
|
223
234
|
|
|
224
235
|
|
|
225
236
|
class HostConfig(msgspec.Struct, kw_only=True):
|
|
226
|
-
ip: str
|
|
237
|
+
ip: str | None = None
|
|
227
238
|
domain_name: str
|
|
228
239
|
user: str
|
|
229
240
|
_envfile: str = msgspec.field(name="envfile")
|
|
@@ -234,6 +245,7 @@ class HostConfig(msgspec.Struct, kw_only=True):
|
|
|
234
245
|
|
|
235
246
|
def __post_init__(self):
|
|
236
247
|
self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
|
|
248
|
+
self.ip = self.ip or self.domain_name
|
|
237
249
|
|
|
238
250
|
def to_dict(self):
|
|
239
251
|
d = {f: getattr(self, f) for f in self.__struct_fields__}
|
fujin/proxies/caddy.py
CHANGED
|
@@ -49,6 +49,9 @@ class WebProxy(msgspec.Struct):
|
|
|
49
49
|
return self.conn.run(*args, **kwargs, pty=True)
|
|
50
50
|
|
|
51
51
|
def install(self):
|
|
52
|
+
result = self.conn.run(f"command -v caddy", warn=True, hide=True)
|
|
53
|
+
if result.ok:
|
|
54
|
+
return
|
|
52
55
|
version = get_latest_gh_tag()
|
|
53
56
|
download_url = GH_DOWNL0AD_URL.format(version=version)
|
|
54
57
|
filename = GH_TAR_FILENAME.format(version=version)
|
|
@@ -71,9 +74,9 @@ class WebProxy(msgspec.Struct):
|
|
|
71
74
|
)
|
|
72
75
|
self.run_pty("sudo systemctl daemon-reload")
|
|
73
76
|
self.run_pty("sudo systemctl enable --now caddy-api")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
self.conn.run(
|
|
78
|
+
"""curl --silent http://localhost:2019/config/ -d '{"apps":{"http": {"servers": {"srv0":{"listen":[":443"]}}}}}' -H 'Content-Type: application/json'"""
|
|
79
|
+
)
|
|
77
80
|
|
|
78
81
|
def uninstall(self):
|
|
79
82
|
self.stop()
|
|
@@ -83,51 +86,31 @@ class WebProxy(msgspec.Struct):
|
|
|
83
86
|
self.run_pty("sudo userdel caddy")
|
|
84
87
|
|
|
85
88
|
def setup(self):
|
|
86
|
-
|
|
89
|
+
current_config = json.loads(
|
|
90
|
+
self.conn.run(
|
|
91
|
+
"curl http://localhost:2019/config/apps/http/servers/srv0", hide=True
|
|
92
|
+
).stdout.strip()
|
|
93
|
+
)
|
|
94
|
+
existing_routes: list[dict] = current_config.get("routes", [])
|
|
95
|
+
new_routes = [r for r in existing_routes if r.get("group") != self.app_name]
|
|
96
|
+
routes = (
|
|
87
97
|
json.loads(self.config_file.read_text())
|
|
88
98
|
if self.config_file.exists()
|
|
89
|
-
else self.
|
|
99
|
+
else self._get_routes()
|
|
90
100
|
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
f"curl localhost:2019/config/apps/http/servers/{self.app_name} -H 'Content-Type: application/json' -d @caddy.json"
|
|
94
|
-
)
|
|
95
|
-
# TODO: stop when received an {"error":"loading config: loading new config: http app module: start: listening on :443: listen tcp :443: bind: permission denied"}, not a 200 ok
|
|
96
|
-
|
|
97
|
-
def teardown(self):
|
|
98
|
-
self.conn.run(f"echo '{json.dumps({})}' > caddy.json")
|
|
101
|
+
new_routes.append(routes)
|
|
102
|
+
current_config["routes"] = new_routes
|
|
99
103
|
self.conn.run(
|
|
100
|
-
f"curl localhost:2019/config/apps/http/servers/
|
|
104
|
+
f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'"
|
|
101
105
|
)
|
|
102
106
|
|
|
103
|
-
def
|
|
104
|
-
self.run_pty("sudo systemctl start caddy-api")
|
|
105
|
-
|
|
106
|
-
def stop(self) -> None:
|
|
107
|
-
self.run_pty("sudo systemctl stop caddy-api")
|
|
108
|
-
|
|
109
|
-
def status(self) -> None:
|
|
110
|
-
self.run_pty("sudo systemctl status caddy-api", warn=True)
|
|
111
|
-
|
|
112
|
-
def restart(self) -> None:
|
|
113
|
-
self.run_pty("sudo systemctl restart caddy-api")
|
|
114
|
-
|
|
115
|
-
def logs(self) -> None:
|
|
116
|
-
self.run_pty(f"sudo journalctl -u caddy-api -f", warn=True)
|
|
117
|
-
|
|
118
|
-
def export_config(self) -> None:
|
|
119
|
-
self.config_file.write_text(json.dumps(self._get_config()))
|
|
120
|
-
|
|
121
|
-
def _get_config(self) -> dict:
|
|
107
|
+
def _get_routes(self) -> dict:
|
|
122
108
|
handle = []
|
|
123
|
-
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"handle": handle,
|
|
129
|
-
}
|
|
130
|
-
],
|
|
109
|
+
routes = {
|
|
110
|
+
"group": self.app_name,
|
|
111
|
+
"match": [{"host": [self.domain_name]}],
|
|
112
|
+
"terminal": True,
|
|
113
|
+
"handle": handle,
|
|
131
114
|
}
|
|
132
115
|
reverse_proxy = {
|
|
133
116
|
"handler": "reverse_proxy",
|
|
@@ -135,14 +118,14 @@ class WebProxy(msgspec.Struct):
|
|
|
135
118
|
}
|
|
136
119
|
if not self.statics:
|
|
137
120
|
handle.append(reverse_proxy)
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
handle.append({"handler": "subroute", "routes":
|
|
121
|
+
return routes
|
|
122
|
+
sub_routes = []
|
|
123
|
+
handle.append({"handler": "subroute", "routes": sub_routes})
|
|
141
124
|
for path, directory in self.statics.items():
|
|
142
125
|
strip_path_prefix = path.replace("/*", "")
|
|
143
126
|
if strip_path_prefix.endswith("/"):
|
|
144
127
|
strip_path_prefix = strip_path_prefix[:-1]
|
|
145
|
-
|
|
128
|
+
sub_routes.append(
|
|
146
129
|
{
|
|
147
130
|
"handle": [
|
|
148
131
|
{
|
|
@@ -170,8 +153,39 @@ class WebProxy(msgspec.Struct):
|
|
|
170
153
|
"match": [{"path": [path]}],
|
|
171
154
|
}
|
|
172
155
|
)
|
|
173
|
-
|
|
174
|
-
return
|
|
156
|
+
sub_routes.append({"handle": [reverse_proxy]})
|
|
157
|
+
return routes
|
|
158
|
+
|
|
159
|
+
def teardown(self):
|
|
160
|
+
current_config = json.loads(
|
|
161
|
+
self.conn.run(
|
|
162
|
+
"curl http://localhost:2019/config/apps/http/servers/srv0"
|
|
163
|
+
).stdout.strip()
|
|
164
|
+
)
|
|
165
|
+
existing_routes: list[dict] = current_config.get("routes", [])
|
|
166
|
+
new_routes = [r for r in existing_routes if r.get("group") != self.app_name]
|
|
167
|
+
current_config["routes"] = new_routes
|
|
168
|
+
self.conn.run(
|
|
169
|
+
f"curl localhost:2019/config/apps/http/servers/srv0 -H 'Content-Type: application/json' -d '{json.dumps(current_config)}'"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def start(self) -> None:
|
|
173
|
+
self.run_pty("sudo systemctl start caddy-api")
|
|
174
|
+
|
|
175
|
+
def stop(self) -> None:
|
|
176
|
+
self.run_pty("sudo systemctl stop caddy-api")
|
|
177
|
+
|
|
178
|
+
def status(self) -> None:
|
|
179
|
+
self.run_pty("sudo systemctl status caddy-api", warn=True)
|
|
180
|
+
|
|
181
|
+
def restart(self) -> None:
|
|
182
|
+
self.run_pty("sudo systemctl restart caddy-api")
|
|
183
|
+
|
|
184
|
+
def logs(self) -> None:
|
|
185
|
+
self.run_pty(f"sudo journalctl -u caddy-api -f", warn=True)
|
|
186
|
+
|
|
187
|
+
def export_config(self) -> None:
|
|
188
|
+
self.config_file.write_text(json.dumps(self._get_routes()))
|
|
175
189
|
|
|
176
190
|
|
|
177
191
|
def get_latest_gh_tag() -> str:
|
fujin/proxies/nginx.py
CHANGED
|
@@ -35,20 +35,20 @@ class WebProxy(msgspec.Struct):
|
|
|
35
35
|
)
|
|
36
36
|
|
|
37
37
|
def run_pty(self, *args, **kwargs):
|
|
38
|
-
return self.
|
|
38
|
+
return self.run_pty(*args, **kwargs, pty=True)
|
|
39
39
|
|
|
40
40
|
def install(self):
|
|
41
|
-
self.
|
|
41
|
+
self.run_pty(
|
|
42
42
|
"sudo apt install -y nginx libpq-dev python3-dev python3-certbot-nginx"
|
|
43
43
|
)
|
|
44
44
|
|
|
45
45
|
def uninstall(self):
|
|
46
46
|
self.stop()
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
47
|
+
self.run_pty("sudo apt remove -y nginx")
|
|
48
|
+
self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
|
|
49
|
+
self.run_pty(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
|
|
50
|
+
self.run_pty("sudo systemctl disable certbot.timer")
|
|
51
|
+
self.run_pty("sudo apt remove -y python3-certbot-nginx")
|
|
52
52
|
|
|
53
53
|
def setup(self):
|
|
54
54
|
conf = (
|
|
@@ -65,11 +65,11 @@ class WebProxy(msgspec.Struct):
|
|
|
65
65
|
)
|
|
66
66
|
if CERTBOT_EMAIL:
|
|
67
67
|
cert_path = f"/etc/letsencrypt/live/{self.domain_name}/fullchain.pem"
|
|
68
|
-
cert_exists = self.
|
|
68
|
+
cert_exists = self.run_pty(f"sudo test -f {cert_path}", warn=True).ok
|
|
69
69
|
|
|
70
70
|
if not cert_exists:
|
|
71
|
-
self.
|
|
72
|
-
f"certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
|
|
71
|
+
self.run_pty(
|
|
72
|
+
f"sudo certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
|
|
73
73
|
)
|
|
74
74
|
self.config_file.parent.mkdir(exist_ok=True)
|
|
75
75
|
self.conn.get(
|
|
@@ -83,9 +83,7 @@ class WebProxy(msgspec.Struct):
|
|
|
83
83
|
def teardown(self):
|
|
84
84
|
self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
|
|
85
85
|
self.run_pty(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
|
|
86
|
-
self.run_pty(
|
|
87
|
-
"sudo systemctl restart nginx",
|
|
88
|
-
)
|
|
86
|
+
self.run_pty("sudo systemctl restart nginx")
|
|
89
87
|
|
|
90
88
|
def start(self) -> None:
|
|
91
89
|
self.run_pty("sudo systemctl start nginx")
|
|
@@ -109,9 +107,9 @@ class WebProxy(msgspec.Struct):
|
|
|
109
107
|
static_locations = ""
|
|
110
108
|
for path, directory in self.statics.items():
|
|
111
109
|
static_locations += f"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
location {path} {{
|
|
111
|
+
alias {directory};
|
|
112
|
+
}}
|
|
115
113
|
"""
|
|
116
114
|
|
|
117
115
|
return f"""
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fujin-cli
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.0
|
|
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
|
|
7
7
|
Project-URL: Source, https://github.com/falcopackages/fujin
|
|
8
8
|
Author-email: Tobi DEGNON <tobidegnon@proton.me>
|
|
9
|
-
License-File: LICENSE.txt
|
|
10
9
|
Keywords: caddy,deployment,django,fastapi,litestar,python,systemd
|
|
11
10
|
Classifier: Development Status :: 3 - Alpha
|
|
12
11
|
Classifier: Intended Audience :: Developers
|
|
@@ -37,16 +36,18 @@ Description-Content-Type: text/markdown
|
|
|
37
36
|
> [!IMPORTANT]
|
|
38
37
|
> This package currently contains minimal features and is a work-in-progress
|
|
39
38
|
|
|
39
|
+
<!-- content:start -->
|
|
40
|
+
|
|
40
41
|
`fujin` is a simple deployment tool that helps you get your project up and running on a VPS in a few minutes. It manages your app processes using `systemd` and runs your apps behind [caddy](https://caddyserver.com/). For Python projects,
|
|
41
42
|
it expects your app to be a packaged Python application ideally with a CLI entry point defined. For other languages, you need to provide a self-contained single executable file with all necessary dependencies.
|
|
42
43
|
The main job of `fujin` is to bootstrap your server (installing caddy, etc.), copy the files onto the server with a structure that supports rollback, and automatically generate configs for systemd and caddy that you can manually edit if needed.
|
|
43
44
|
|
|
44
|
-
Check out the [documentation📚](https://fujin.
|
|
45
|
+
Check out the [documentation📚](https://fujin.oluwatobi.dev/en/latest/) for installation, features, and usage guides.
|
|
45
46
|
|
|
46
47
|
## Why?
|
|
47
48
|
|
|
48
49
|
I wanted [kamal](https://kamal-deploy.org/) but without Docker, and I thought the idea was fun. At its core, this project automates versions of this [tutorial](https://www.digitalocean.com/community/tutorials/how-to-set-up-django-with-postgres-nginx-and-gunicorn-on-ubuntu). If you've been a Django beginner
|
|
49
|
-
trying to get your app in production, you probably went through this. I'm using caddy instead of nginx because
|
|
50
|
+
trying to get your app in production, you probably went through this. I'm using caddy instead of nginx because it's configurable via an API and it's is a no-brainer for SSL certificates. Systemd is the default on most Linux distributions and does a good enough job.
|
|
50
51
|
|
|
51
52
|
Fujin was initially planned to be a Python-only project, but the core concepts can be applied to any language that can produce a single distributable file (e.g., Go, Rust). I wanted to recreate kamal's nice local-to-remote app management API, but I'm skipping Docker to keep things simple.
|
|
52
53
|
I'm currently rocking SQLite in production for my side projects and ths setup is enough for my use case.
|
|
@@ -64,3 +65,5 @@ Fujin draws inspiration from the following tools for their developer experience.
|
|
|
64
65
|
## License
|
|
65
66
|
|
|
66
67
|
`fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
|
|
68
|
+
|
|
69
|
+
<!-- content:end -->
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
|
|
3
|
-
fujin/config.py,sha256=
|
|
3
|
+
fujin/config.py,sha256=GecU6XklYNXjjR7bkRORBcX1-AYlIaTMw1mmYKSCSnA,9595
|
|
4
4
|
fujin/connection.py,sha256=ZkYaNykRFj9Yr-K-vOrZtVVGUDurDm6W7OQrgct71CA,2428
|
|
5
5
|
fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
|
|
6
6
|
fujin/hooks.py,sha256=QHIqxLxujG2U70UkN1BpUplE6tTqn7pFJP5oHde1tUQ,1350
|
|
7
7
|
fujin/commands/__init__.py,sha256=uIGGXt8YofL5RZn8KIy153ioWGoCl32ffHtqOhB-6ZM,78
|
|
8
8
|
fujin/commands/_base.py,sha256=o3R4-c3XeFWTIW3stiUdrcCPwdjzfjUVIpZy2L1-gZ4,2525
|
|
9
|
-
fujin/commands/app.py,sha256=
|
|
10
|
-
fujin/commands/config.py,sha256=
|
|
11
|
-
fujin/commands/deploy.py,sha256=
|
|
9
|
+
fujin/commands/app.py,sha256=mazb4dCTdR5juh79bL3a9b68Nd6O8u_nR9IgYqQlqWE,5279
|
|
10
|
+
fujin/commands/config.py,sha256=xdfd1OZLxw2YZldiAbW5rq5EBXEaXbUC-I7FKLRfzIQ,2387
|
|
11
|
+
fujin/commands/deploy.py,sha256=oAOLUrtHjHkmIA3D0AiaJRWEsfVk51_I29kRC_FhjYo,5463
|
|
12
12
|
fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
|
|
13
|
-
fujin/commands/down.py,sha256=
|
|
14
|
-
fujin/commands/init.py,sha256=
|
|
15
|
-
fujin/commands/proxy.py,sha256=
|
|
13
|
+
fujin/commands/down.py,sha256=v1lAq70ApktjeHRB_1sCzjmKH8t6EXqyL4RTt7OE-f0,1716
|
|
14
|
+
fujin/commands/init.py,sha256=t8uwwOi4SBqHjV8px_SkTHAeZIiIUJnFN-lf7DK6HhE,3959
|
|
15
|
+
fujin/commands/proxy.py,sha256=ajXwboS0gDDiMWW7b9rtWU6WPF1h7JYYeycDyU-hQfg,3053
|
|
16
16
|
fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
|
|
17
|
-
fujin/commands/redeploy.py,sha256=
|
|
17
|
+
fujin/commands/redeploy.py,sha256=JvCJBZBcCKkUw1efZwRPJMLUAV8oqBAZeSbUBLHyn3k,2185
|
|
18
18
|
fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
|
|
19
19
|
fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
|
|
20
|
-
fujin/commands/server.py,sha256=
|
|
20
|
+
fujin/commands/server.py,sha256=0N_P_Luj31t56riZ8GfgRqW3vRHiw0cDrlp3PFoyWn8,3453
|
|
21
21
|
fujin/commands/up.py,sha256=DgDN-1mc_mMHJRCIvcB947Cd5a7phunu9NpXloGK0UU,419
|
|
22
22
|
fujin/process_managers/__init__.py,sha256=MhhfTBhm64zWRAKgjvsZRIToOUJus60vGScbAjqpQ6Y,994
|
|
23
23
|
fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAPBObk,5603
|
|
24
24
|
fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
|
|
25
|
-
fujin/proxies/caddy.py,sha256=
|
|
25
|
+
fujin/proxies/caddy.py,sha256=dzLD8s664_kIK-1hCE3y50JIwBd8kK9yS1LynUDRVSE,7908
|
|
26
26
|
fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
|
|
27
|
-
fujin/proxies/nginx.py,sha256=
|
|
27
|
+
fujin/proxies/nginx.py,sha256=S2-tBaytGtehqMyeZZMPSPoXjV1GVv7S63eMtfhkGNM,4100
|
|
28
28
|
fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
|
|
29
29
|
fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
|
|
30
30
|
fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
|
|
31
|
-
fujin_cli-0.
|
|
32
|
-
fujin_cli-0.
|
|
33
|
-
fujin_cli-0.
|
|
34
|
-
fujin_cli-0.
|
|
35
|
-
fujin_cli-0.
|
|
31
|
+
fujin_cli-0.6.0.dist-info/METADATA,sha256=_rYue3Q7z_StHy_Z9DVdaU0RWijde8eWFlm_gwxN4GY,4452
|
|
32
|
+
fujin_cli-0.6.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
|
|
33
|
+
fujin_cli-0.6.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
|
|
34
|
+
fujin_cli-0.6.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
|
|
35
|
+
fujin_cli-0.6.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|