fujin-cli 0.1.0__py3-none-any.whl → 0.2.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/config.py ADDED
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import msgspec
8
+
9
+ from .errors import ImproperlyConfiguredError
10
+
11
+ if sys.version_info >= (3, 11):
12
+ import tomllib
13
+ else:
14
+ import tomli as tomllib
15
+
16
+ try:
17
+ from enum import StrEnum
18
+ except ImportError:
19
+ from enum import Enum
20
+
21
+ class StrEnum(str, Enum):
22
+ pass
23
+
24
+
25
+ class Hook(StrEnum):
26
+ PRE_DEPLOY = "pre_deploy"
27
+
28
+
29
+ class Config(msgspec.Struct, kw_only=True):
30
+ app: str
31
+ app_bin: str = ".venv/bin/{app}"
32
+ version: str = msgspec.field(default_factory=lambda: read_version_from_pyproject())
33
+ python_version: str = msgspec.field(default_factory=lambda: find_python_version())
34
+ build_command: str
35
+ _distfile: str = msgspec.field(name="distfile")
36
+ aliases: dict[str, str]
37
+ hosts: dict[str, HostConfig]
38
+ processes: dict[str, str]
39
+ process_manager: str = "fujin.process_managers.systemd"
40
+ webserver: Webserver
41
+ _requirements: str = msgspec.field(name="requirements", default="requirements.txt")
42
+ hooks: dict[Hook, str] = msgspec.field(default=dict)
43
+
44
+ def __post_init__(self):
45
+ self.app_bin = self.app_bin.format(app=self.app)
46
+ self._distfile = self._distfile.format(version=self.version)
47
+
48
+ if "web" not in self.processes and self.webserver.type != "fujin.proxies.dummy":
49
+ raise ValueError(
50
+ "Missing web process or set the proxy to 'fujin.proxies.dummy' to disable the use of a proxy"
51
+ )
52
+
53
+ @property
54
+ def distfile(self) -> Path:
55
+ return Path(self._distfile)
56
+
57
+ @property
58
+ def requirements(self) -> Path:
59
+ return Path(self._requirements)
60
+
61
+ @classmethod
62
+ def read(cls) -> Config:
63
+ fujin_toml = Path("fujin.toml")
64
+ if not fujin_toml.exists():
65
+ raise ImproperlyConfiguredError(
66
+ "No fujin.toml file found in the current directory"
67
+ )
68
+ try:
69
+ return msgspec.toml.decode(fujin_toml.read_text(), type=cls)
70
+ except msgspec.ValidationError as e:
71
+ raise ImproperlyConfiguredError(f"Improperly configured, {e}") from e
72
+
73
+
74
+ class HostConfig(msgspec.Struct, kw_only=True):
75
+ ip: str
76
+ domain_name: str
77
+ user: str
78
+ _envfile: str = msgspec.field(name="envfile")
79
+ projects_dir: str = "/home/{user}/.local/share/fujin"
80
+ password_env: str | None = None
81
+ ssh_port: int = 22
82
+ _key_filename: str | None = msgspec.field(name="key_filename", default=None)
83
+ default: bool = False
84
+
85
+ def __post_init__(self):
86
+ self.projects_dir = self.projects_dir.format(user=self.user)
87
+
88
+ def to_dict(self):
89
+ return {f: getattr(self, f) for f in self.__struct_fields__}
90
+
91
+ @property
92
+ def envfile(self) -> Path:
93
+ return Path(self._envfile)
94
+
95
+ @property
96
+ def key_filename(self) -> Path | None:
97
+ if self._key_filename:
98
+ return Path(self._key_filename)
99
+
100
+ @property
101
+ def password(self) -> str | None:
102
+ if not self.password_env:
103
+ return
104
+ password = os.getenv(self.password_env)
105
+ if not password:
106
+ msg = f"Env {self.password_env} can not be found"
107
+ raise ImproperlyConfiguredError(msg)
108
+ return password
109
+
110
+
111
+ class Webserver(msgspec.Struct):
112
+ upstream: str
113
+ type: str = "fujin.proxies.caddy"
114
+
115
+
116
+ def read_version_from_pyproject():
117
+ try:
118
+ return tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
119
+ except (FileNotFoundError, KeyError) as e:
120
+ raise msgspec.ValidationError(
121
+ "Project version was not found in the pyproject.toml file, define it manually"
122
+ ) from e
123
+
124
+
125
+ def find_python_version():
126
+ py_version_file = Path(".python-version")
127
+ if not py_version_file.exists():
128
+ raise msgspec.ValidationError(
129
+ f"Add a python_version key or a .python-version file"
130
+ )
131
+ return py_version_file.read_text().strip()
fujin/errors.py ADDED
@@ -0,0 +1,5 @@
1
+ import cappa
2
+
3
+
4
+ class ImproperlyConfiguredError(cappa.Exit):
5
+ code = 1
fujin/hooks.py ADDED
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+
3
+ from fujin.config import Config
4
+ from fujin.host import Host
5
+ from fujin.config import Hook
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class HookManager:
10
+ config: Config
11
+ host: Host
12
+
13
+ def pre_deploy(self):
14
+ if pre_deploy := self.config.hooks.get(Hook.PRE_DEPLOY):
15
+ self.host.run(pre_deploy)
fujin/host.py ADDED
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from contextlib import contextmanager
4
+ from dataclasses import dataclass
5
+ from functools import cached_property
6
+
7
+ import cappa
8
+ from fabric import Connection
9
+ from invoke import Responder
10
+ from paramiko.ssh_exception import AuthenticationException
11
+
12
+ from .config import HostConfig
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Host:
17
+ config: HostConfig
18
+
19
+ def __str__(self):
20
+ return self.config.ip
21
+
22
+ @property
23
+ def watchers(self) -> list[Responder]:
24
+ if not self.config.password:
25
+ return []
26
+ return [
27
+ Responder(
28
+ pattern=r"\[sudo\] password:",
29
+ response=f"{self.config.password}\n",
30
+ )
31
+ ]
32
+
33
+ @cached_property
34
+ def connection(self) -> Connection:
35
+ connect_kwargs = None
36
+ if self.config.key_filename:
37
+ connect_kwargs = {"key_filename": str(self.config.key_filename)}
38
+ elif self.config.password:
39
+ connect_kwargs = {"password": self.config.password}
40
+
41
+ return Connection(
42
+ self.config.ip,
43
+ user=self.config.user,
44
+ port=self.config.ssh_port,
45
+ connect_kwargs=connect_kwargs,
46
+ )
47
+
48
+ def run(self, args: str, **kwargs):
49
+ try:
50
+ return self.connection.run(args, **kwargs, watchers=self.watchers)
51
+ except AuthenticationException as e:
52
+ msg = f"Authentication failed for {self.config.user}@{self.config.ip} -p {self.config.ssh_port}.\n"
53
+ if self.config.key_filename:
54
+ msg += f"An SSH key was provided at {self.config.key_filename.resolve()}. Please verify its validity and correctness."
55
+ elif self.config.password:
56
+ msg += f"A password was provided through the environment variable {self.config.password_env}. Please ensure it is correct for the user {self.config.user}."
57
+ else:
58
+ msg += "No password or SSH key was provided. Ensure your current host has SSH access to the target host."
59
+ raise cappa.Exit(msg, code=1) from e
60
+
61
+ def put(self, *args, **kwargs):
62
+ return self.connection.put(args, **kwargs, watchers=self.watchers)
63
+
64
+ def get(self, *args, **kwargs):
65
+ return self.connection.get(args, **kwargs, watchers=self.watchers)
66
+
67
+ def sudo(self, args: str, **kwargs):
68
+ return self.connection.sudo(args, **kwargs)
69
+
70
+ def run_uv(self, args: str, **kwargs):
71
+ return self.run(f"/home/{self.config.user}/.cargo/bin/uv {args}", **kwargs)
72
+
73
+ def run_caddy(self, args: str, **kwargs):
74
+ return self.run(f"/home/{self.config.user}/.local/bin/caddy {args}", **kwargs)
75
+
76
+ def make_project_dir(self, project_name: str):
77
+ self.run(f"mkdir -p {self.project_dir(project_name)}")
78
+
79
+ def project_dir(self, project_name: str) -> str:
80
+ return f"{self.config.projects_dir}/{project_name}"
81
+
82
+ @contextmanager
83
+ def cd_project_dir(self, project_name: str):
84
+ with self.connection.cd(self.project_dir(project_name)):
85
+ yield
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Protocol
4
+
5
+ if TYPE_CHECKING:
6
+ from fujin.config import Config
7
+ from fujin.host import Host
8
+
9
+
10
+ class ProcessManager(Protocol):
11
+ host: Host
12
+ config: Config
13
+ service_names: list[str]
14
+
15
+ def get_service_name(self, name: str): ...
16
+
17
+ def install_services(self) -> None: ...
18
+
19
+ def uninstall_services(self) -> None: ...
20
+
21
+ def start_services(self, *names) -> None: ...
22
+
23
+ def restart_services(self, *names) -> None: ...
24
+
25
+ def stop_services(self, *names) -> None: ...
26
+
27
+ def service_logs(self, name: str, follow: bool = False): ...
28
+
29
+ def reload_configuration(self) -> None: ...
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ from fujin.config import Config
8
+ from fujin.host import Host
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class SystemdFile:
13
+ name: str
14
+ body: str
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class ProcessManager:
19
+ config: Config
20
+ host: Host
21
+
22
+ @property
23
+ def service_names(self) -> list[str]:
24
+ return [self.get_service_name(name) for name in self.config.processes]
25
+
26
+ def get_service_name(self, name: str):
27
+ if name == "web":
28
+ return self.config.app
29
+ return f"{self.config.app}-{name}.service"
30
+
31
+ def install_services(self) -> None:
32
+ conf_files = self.get_configuration_files()
33
+ for conf_file in conf_files:
34
+ self.host.sudo(
35
+ f"echo '{conf_file.body}' | sudo tee /etc/systemd/system/{conf_file.name}",
36
+ hide="out",
37
+ )
38
+
39
+ self.host.sudo(f"systemctl enable --now {self.config.app}.socket")
40
+ for name in self.service_names:
41
+ # the main web service is launched by the socket service
42
+ if name != f"{self.config.app}.service":
43
+ self.host.sudo(f"systemctl enable {name}")
44
+
45
+ def get_configuration_files(self) -> list[SystemdFile]:
46
+ templates_folder = (
47
+ Path(importlib.util.find_spec("fujin").origin).parent / "templates"
48
+ )
49
+ web_service_content = (templates_folder / "web.service").read_text()
50
+ web_socket_content = (templates_folder / "web.socket").read_text()
51
+ other_service_content = (templates_folder / "other.service").read_text()
52
+ context = {
53
+ "app": self.config.app,
54
+ "user": self.host.config.user,
55
+ "project_dir": self.host.project_dir(self.config.app),
56
+ }
57
+
58
+ files = []
59
+ for name, command in self.config.processes.items():
60
+ name = self.get_service_name(name)
61
+ if name == "web":
62
+ body = web_service_content.format(**context, command=command)
63
+ files.append(
64
+ SystemdFile(
65
+ name=f"{self.config.app}.socket",
66
+ body=web_socket_content.format(**context),
67
+ )
68
+ )
69
+ else:
70
+ body = other_service_content.format(**context, command=command)
71
+ files.append(SystemdFile(name=name, body=body))
72
+ return files
73
+
74
+ def uninstall_services(self) -> None:
75
+ self.stop_services()
76
+ self.host.sudo(f"systemctl disable {self.config.app}.socket")
77
+ for name in self.service_names:
78
+ # was never enabled in the first place, look at the code above
79
+ if name != f"{self.config.app}.service":
80
+ self.host.sudo(f"systemctl disable {name}")
81
+
82
+ def start_services(self, *names) -> None:
83
+ names = names or self.service_names
84
+ for name in names:
85
+ if name in self.service_names:
86
+ self.host.sudo(f"systemctl start {name}")
87
+
88
+ def restart_services(self, *names) -> None:
89
+ names = names or self.service_names
90
+ for name in names:
91
+ if name in self.service_names:
92
+ self.host.sudo(f"systemctl restart {name}")
93
+
94
+ def stop_services(self, *names) -> None:
95
+ names = names or self.service_names
96
+ for name in names:
97
+ if name in self.service_names:
98
+ self.host.sudo(f"systemctl stop {name}")
99
+
100
+ def service_logs(self, name: str, follow: bool = False):
101
+ # TODO: add more options here
102
+ self.host.sudo(f"journalctl -u {name} -r {'-f' if follow else ''}")
103
+
104
+ def reload_configuration(self) -> None:
105
+ self.host.sudo(f"systemctl daemon-reload")
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Protocol
4
+
5
+ if TYPE_CHECKING:
6
+ from fujin.config import Config
7
+ from fujin.host import Host
8
+
9
+
10
+ class WebProxy(Protocol):
11
+ host: Host
12
+ config: Config
13
+
14
+ def install(self) -> None: ...
15
+
16
+ def setup(self) -> None: ...
17
+
18
+ def teardown(self) -> None: ...
fujin/proxies/caddy.py ADDED
@@ -0,0 +1,52 @@
1
+ import json
2
+
3
+ import msgspec
4
+
5
+ from fujin.config import Config
6
+ from fujin.host import Host
7
+
8
+
9
+ class WebProxy(msgspec.Struct):
10
+ host: Host
11
+ config: Config
12
+
13
+ def install(self):
14
+ self.host.run_uv("tool install caddy-bin")
15
+ self.host.run_caddy("start", pty=True)
16
+
17
+ def setup(self):
18
+ with self.host.cd_project_dir(self.config.app):
19
+ self.host.run(f"echo '{json.dumps(self._generate_config())}' > caddy.json")
20
+ self.host.run(
21
+ f"curl localhost:2019/load -H 'Content-Type: application/json' -d @caddy.json"
22
+ )
23
+
24
+ def teardown(self):
25
+ # TODO
26
+ pass
27
+
28
+ def _generate_config(self) -> dict:
29
+ return {
30
+ "apps": {
31
+ "http": {
32
+ "servers": {
33
+ self.config.app: {
34
+ "listen": [":443"],
35
+ "routes": [
36
+ {
37
+ "match": [{"host": [self.host.config.domain_name]}],
38
+ "handle": [
39
+ {
40
+ "handler": "reverse_proxy",
41
+ "upstreams": [
42
+ {"dial": self.config.webserver.upstream}
43
+ ],
44
+ }
45
+ ],
46
+ }
47
+ ],
48
+ }
49
+ }
50
+ }
51
+ }
52
+ }
fujin/proxies/dummy.py ADDED
@@ -0,0 +1,16 @@
1
+ from fujin.config import Config
2
+ from fujin.host import Host
3
+
4
+
5
+ class WebProxy:
6
+ host: Host
7
+ config: Config
8
+
9
+ def install(self):
10
+ pass
11
+
12
+ def setup(self):
13
+ pass
14
+
15
+ def teardown(self):
16
+ pass
fujin/proxies/nginx.py ADDED
@@ -0,0 +1,59 @@
1
+ import msgspec
2
+
3
+ from fujin.config import Config
4
+ from fujin.host import Host
5
+
6
+ CERTBOT_EMAIL = ""
7
+
8
+ # TODO: this is a wip
9
+
10
+
11
+ class WebProxy(msgspec.Struct):
12
+ host: Host
13
+ config: Config
14
+
15
+ def install(self):
16
+ # TODO: won"t always install the latest version, install certbot with uv ?
17
+ self.host.sudo(
18
+ "apt install -y nginx libpq-dev python3-dev python3-certbot-nginx sqlite3"
19
+ )
20
+
21
+ def setup(self):
22
+ self.host.sudo(
23
+ f"echo '{self._get_config()}' | sudo tee /etc/nginx/sites-available/{self.config.app}",
24
+ hide="out",
25
+ )
26
+ self.host.sudo(
27
+ f"ln -sf /etc/nginx/sites-available/{self.config.app} /etc/nginx/sites-enabled/{self.config.app}"
28
+ )
29
+ self.host.sudo(
30
+ f"certbot --nginx -d {self.host.config.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
31
+ )
32
+ # Updating local Nginx configuration
33
+ self.host.get(
34
+ f"/etc/nginx/sites-available/{self.config.app}",
35
+ f".fujin/{self.config.app}",
36
+ )
37
+ # Enabling certificate auto-renewal
38
+ self.host.sudo("systemctl enable certbot.timer")
39
+ self.host.sudo("systemctl start certbot.timer")
40
+
41
+ def teardown(self):
42
+ pass
43
+
44
+ def _get_config(self) -> str:
45
+ return f"""
46
+ server {{
47
+ listen 80;
48
+ server_name {self.host.config.domain_name};
49
+
50
+ location / {{
51
+ proxy_pass {self.config.webserver.upstream};
52
+ proxy_set_header Host $host;
53
+ proxy_set_header X-Real-IP $remote_addr;
54
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
55
+ proxy_set_header X-Forwarded-Proto $scheme;
56
+ }}
57
+ }}
58
+
59
+ """
@@ -0,0 +1,15 @@
1
+ [Unit]
2
+ Description={app} Worker
3
+
4
+ [Service]
5
+ User={user}
6
+ Group=www-data
7
+ WorkingDirectory={project_dir}
8
+ ExecStart={command}
9
+ EnvironmentFile={project_dir}/.env
10
+ Restart=always
11
+ #StandardOutput=append:/var/log/your_project/qcluster.out.log
12
+ #StandardError=append:/var/log/your_project/qcluster.err.log
13
+
14
+ [Install]
15
+ WantedBy=multi-user.target
@@ -0,0 +1,24 @@
1
+ # TODO: add tons of comments in this file
2
+ [Unit]
3
+ Description={app} daemon
4
+ Requires={app}.socket
5
+ After=network.target
6
+
7
+ [Service]
8
+ Type=notify
9
+ NotifyAccess=main
10
+ User={user}
11
+ Group=www-data
12
+ RuntimeDirectory={app}
13
+ WorkingDirectory={project_dir}
14
+ ExecStart={project_dir}/{command}
15
+ EnvironmentFile={project_dir}/.env
16
+ ExecReload=/bin/kill -s HUP $MAINPID
17
+ KillMode=mixed
18
+ TimeoutStopSec=5
19
+ PrivateTmp=true
20
+ # if your app does not need administrative capabilities, let systemd know
21
+ # ProtectSystem=strict
22
+
23
+ [Install]
24
+ WantedBy=multi-user.target
@@ -0,0 +1,11 @@
1
+ [Unit]
2
+ Description={app} socket
3
+
4
+ [Socket]
5
+ ListenStream=/run/{app}.sock
6
+ SocketUser=www-data
7
+ SocketGroup=www-data
8
+ SocketMode=0660
9
+
10
+ [Install]
11
+ WantedBy=sockets.target
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.3
2
+ Name: fujin-cli
3
+ Version: 0.2.0
4
+ Summary: Add your description here
5
+ Project-URL: Documentation, https://github.com/falcopackages/fujin#readme
6
+ Project-URL: Issues, https://github.com/falcopackages/fujin/issues
7
+ Project-URL: Source, https://github.com/falcopackages/fujin
8
+ Author-email: Tobi DEGNON <tobidegnon@proton.me>
9
+ License-File: LICENSE.txt
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: Implementation :: CPython
19
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: cappa>=0.24.0
22
+ Requires-Dist: fabric>=3.2.2
23
+ Requires-Dist: msgspec[toml]>=0.18.6
24
+ Requires-Dist: rich>=13.9.2
25
+ Description-Content-Type: text/markdown
26
+
27
+ # fujin
28
+
29
+ [![PyPI - Version](https://img.shields.io/pypi/v/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
30
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
31
+
32
+ -----
33
+
34
+ > [!IMPORTANT]
35
+ > This package currently contains no features and is a work-in-progress
36
+
37
+
38
+ **Table of Contents**
39
+
40
+ - [fujin](#fujin)
41
+ - [Installation](#installation)
42
+ - [License](#license)
43
+
44
+ ## Installation
45
+
46
+ ```console
47
+ pip install fujin-cli
48
+ ```
49
+
50
+ ## License
51
+
52
+ `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,29 @@
1
+ fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ fujin/__main__.py,sha256=cmbkKqDbGyc_himfDstDjC5SRi07-rU3oxWgoL_v_6c,1278
3
+ fujin/config.py,sha256=5g_KYGc3IDdHLZRLYiw55hGob85Yxovm-47JHiEXcYE,3896
4
+ fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
5
+ fujin/hooks.py,sha256=Nmi0psj0cwomSaJM6l0r2V5iVn1x0xdrazW1CRQ2wyw,346
6
+ fujin/host.py,sha256=uNYeYO2J5PQ7U3W8Olng7MuRK6uzIoom2yDNX914F14,3023
7
+ fujin/commands/__init__.py,sha256=-9mrBJHtwQELh58HlMEjCE60nowtGzySy4-inph3YY0,78
8
+ fujin/commands/_base.py,sha256=IYK7rXB5QW2Ac4pcgafQTjB1SPPht1ERWE_5WFzdSfo,2765
9
+ fujin/commands/app.py,sha256=Cu3EW1FbOqR40ArsbBpwA3nLCX41leys398obc5lRbM,2347
10
+ fujin/commands/config.py,sha256=bfWCyGYz-n-VNtcs-HiKXB7eP-K_YQkBGTPAqetVYYU,6136
11
+ fujin/commands/deploy.py,sha256=gHrqPdZ8btB9CcwjVv1BDwbeD0RIoMALIeh7cNzUsm8,2032
12
+ fujin/commands/down.py,sha256=iUneVfELnWmh-9NqVGCAwhF1nGSp4mZQAmW6_ER_DTg,1109
13
+ fujin/commands/redeploy.py,sha256=3xtExNvNHDoXgWsY3-evZnBHAEaGKRVyAB4h-9YiVpM,533
14
+ fujin/commands/server.py,sha256=UOl4pQWfj_sCc5l5EBx2PQ8UqIrMuEBJyuT-DvXFK1E,2186
15
+ fujin/commands/up.py,sha256=kkHbNSzTogA_vGqPwzK8a85Hey3rPHSmz2YRqWeQ100,450
16
+ fujin/process_managers/__init__.py,sha256=izJCtaGO-Fwptx4NHHjp8eOFuWfgjRMjt6YiQ0ZkXbA,672
17
+ fujin/process_managers/systemd.py,sha256=Ltt9i1EaY2Ip4WdbcSmuRVvv-Z04lm5izuOT3hxJB1M,3770
18
+ fujin/proxies/__init__.py,sha256=j3YdWdDBAfJ0-EL4APR0gkwTFkZ0BcUJQ_wg8bWVsGE,335
19
+ fujin/proxies/caddy.py,sha256=ehngeBOpUWqR7bJLjdVtoMhEqwStS3FZrZBzzhdA0-g,1620
20
+ fujin/proxies/dummy.py,sha256=Ur1mNxQGBmVLNJ0PV2ODR0fDw8VDItJ6-P1GOUc6o1o,222
21
+ fujin/proxies/nginx.py,sha256=1vNzxtulx6tWe-lW5JV9fK7XrZWdf9_v-VjyHdgHEXQ,1686
22
+ fujin/templates/other.service,sha256=XJUaQ0MxRuqT6KNkcn5ZPrjVtWh1zY9qbOzIhxGYOms,331
23
+ fujin/templates/web.service,sha256=TVMELmlOVo_SZxUqkpyizvZeX0T4b1sL1WVpbJ0D-DE,528
24
+ fujin/templates/web.socket,sha256=W5EtR-J1GNMaMqZDQduSjG2JY7gFFPKEDpPolFus9c0,163
25
+ fujin_cli-0.2.0.dist-info/METADATA,sha256=ahTCcNWwl7V9Zs0RWVH1UJ_L0EURdJliSr7d-wuK1Js,1660
26
+ fujin_cli-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
27
+ fujin_cli-0.2.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
28
+ fujin_cli-0.2.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
29
+ fujin_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fujin = fujin.__main__:main