fujin-cli 0.2.0__py3-none-any.whl → 0.4.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/proxies/caddy.py CHANGED
@@ -1,52 +1,217 @@
1
+ from __future__ import annotations
2
+
1
3
  import json
4
+ import urllib.request
5
+ from pathlib import Path
2
6
 
3
7
  import msgspec
4
8
 
5
9
  from fujin.config import Config
6
- from fujin.host import Host
10
+ from fujin.connection import Connection
11
+
12
+ DEFAULT_VERSION = "2.8.4"
13
+ GH_TAR_FILENAME = "caddy_{version}_linux_amd64.tar.gz"
14
+ GH_DOWNL0AD_URL = (
15
+ "https://github.com/caddyserver/caddy/releases/download/v{version}/"
16
+ + GH_TAR_FILENAME
17
+ )
18
+ GH_RELEASE_LATEST_URL = "https://api.github.com/repos/caddyserver/caddy/releases/latest"
19
+
20
+
21
+ # TODO: let the user write the configuration with a simple syntax and export use caddy adapter, same for exporting,
22
+ # don't export to json
7
23
 
8
24
 
9
25
  class WebProxy(msgspec.Struct):
10
- host: Host
11
- config: Config
26
+ conn: Connection
27
+ domain_name: str
28
+ app_name: str
29
+ upstream: str
30
+ statics: dict[str, str]
31
+ local_config_dir: Path
32
+
33
+ @property
34
+ def config_file(self) -> Path:
35
+ return self.local_config_dir / "caddy.json"
36
+
37
+ @classmethod
38
+ def create(cls, config: Config, conn: Connection) -> WebProxy:
39
+ return cls(
40
+ conn=conn,
41
+ domain_name=config.host.domain_name,
42
+ upstream=config.webserver.upstream,
43
+ app_name=config.app_name,
44
+ local_config_dir=config.local_config_dir,
45
+ statics=config.webserver.statics,
46
+ )
47
+
48
+ def run_pty(self, *args, **kwargs):
49
+ return self.conn.run(*args, **kwargs, pty=True)
12
50
 
13
51
  def install(self):
14
- self.host.run_uv("tool install caddy-bin")
15
- self.host.run_caddy("start", pty=True)
52
+ version = get_latest_gh_tag()
53
+ download_url = GH_DOWNL0AD_URL.format(version=version)
54
+ filename = GH_TAR_FILENAME.format(version=version)
55
+ with self.conn.cd("/tmp"):
56
+ self.conn.run(f"curl -O -L {download_url}")
57
+ self.conn.run(f"tar -xzvf {filename}")
58
+ self.run_pty("sudo mv caddy /usr/bin/")
59
+ self.conn.run(f"rm {filename}")
60
+ self.conn.run("rm LICENSE && rm README.md")
61
+ self.run_pty("sudo groupadd --force --system caddy")
62
+ self.conn.run(
63
+ "sudo useradd --system --gid caddy --create-home --home-dir /var/lib/caddy --shell /usr/sbin/nologin --comment 'Caddy web server' caddy",
64
+ pty=True,
65
+ warn=True,
66
+ )
67
+ self.conn.run(
68
+ f"echo '{systemd_service}' | sudo tee /etc/systemd/system/caddy-api.service",
69
+ hide="out",
70
+ pty=True,
71
+ )
72
+ self.run_pty("sudo systemctl daemon-reload")
73
+ self.run_pty("sudo systemctl enable --now caddy-api")
74
+ # to initialize the caddy config, when running setup on a fresh caddy setup it fails because the key config/apps was not previously defined
75
+ # TODO this will reset the config of any existing server, should probably do I check before running this
76
+ self.conn.run("""curl --silent http://localhost:2019/config/ -d '{"apps":{"http": {"servers": {}}}}' -H 'Content-Type: application/json'""")
77
+
78
+ def uninstall(self):
79
+ self.stop()
80
+ self.run_pty("sudo systemctl disable caddy-api")
81
+ self.run_pty("sudo rm /usr/bin/caddy")
82
+ self.run_pty("sudo rm /etc/systemd/system/caddy-api.service")
83
+ self.run_pty("sudo userdel caddy")
16
84
 
17
85
  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
- )
86
+ config = (
87
+ json.loads(self.config_file.read_text())
88
+ if self.config_file.exists()
89
+ else self._get_config()
90
+ )
91
+ self.conn.run(f"echo '{json.dumps(config)}' > caddy.json")
92
+ self.conn.run(
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
23
96
 
24
97
  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"],
98
+ self.conn.run(f"echo '{json.dumps({})}' > caddy.json")
99
+ self.conn.run(
100
+ f"curl localhost:2019/config/apps/http/servers/{self.app_name} -H 'Content-Type: application/json' -d @caddy.json"
101
+ )
102
+
103
+ def start(self) -> None:
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:
122
+ handle = []
123
+ config = {
124
+ "listen": [":443"],
125
+ "routes": [
126
+ {
127
+ "match": [{"host": [self.domain_name]}],
128
+ "handle": handle,
129
+ }
130
+ ],
131
+ }
132
+ reverse_proxy = {
133
+ "handler": "reverse_proxy",
134
+ "upstreams": [{"dial": self.upstream}],
135
+ }
136
+ if not self.statics:
137
+ handle.append(reverse_proxy)
138
+ return config
139
+ routes = []
140
+ handle.append({"handler": "subroute", "routes": routes})
141
+ for path, directory in self.statics.items():
142
+ strip_path_prefix = path.replace("/*", "")
143
+ if strip_path_prefix.endswith("/"):
144
+ strip_path_prefix = strip_path_prefix[:-1]
145
+ routes.append(
146
+ {
147
+ "handle": [
148
+ {
149
+ "handler": "subroute",
35
150
  "routes": [
36
151
  {
37
- "match": [{"host": [self.host.config.domain_name]}],
38
152
  "handle": [
39
153
  {
40
- "handler": "reverse_proxy",
41
- "upstreams": [
42
- {"dial": self.config.webserver.upstream}
43
- ],
154
+ "handler": "rewrite",
155
+ "strip_path_prefix": strip_path_prefix,
44
156
  }
45
- ],
46
- }
157
+ ]
158
+ },
159
+ {
160
+ "handle": [
161
+ {"handler": "vars", "root": directory},
162
+ {
163
+ "handler": "file_server",
164
+ },
165
+ ]
166
+ },
47
167
  ],
48
168
  }
49
- }
169
+ ],
170
+ "match": [{"path": [path]}],
50
171
  }
51
- }
52
- }
172
+ )
173
+ routes.append({"handle": [reverse_proxy]})
174
+ return config
175
+
176
+
177
+ def get_latest_gh_tag() -> str:
178
+ with urllib.request.urlopen(GH_RELEASE_LATEST_URL) as response:
179
+ if response.status != 200:
180
+ return DEFAULT_VERSION
181
+ try:
182
+ data = json.loads(response.read().decode())
183
+ return data["tag_name"][1:]
184
+ except (KeyError, json.JSONDecodeError):
185
+ return DEFAULT_VERSION
186
+
187
+
188
+ systemd_service = """
189
+ # caddy-api.service
190
+ #
191
+ # For using Caddy with its API.
192
+ #
193
+ # This unit is "durable" in that it will automatically resume
194
+ # the last active configuration if the service is restarted.
195
+ #
196
+ # See https://caddyserver.com/docs/install for instructions.
197
+
198
+ [Unit]
199
+ Description=Caddy
200
+ Documentation=https://caddyserver.com/docs/
201
+ After=network.target network-online.target
202
+ Requires=network-online.target
203
+
204
+ [Service]
205
+ Type=notify
206
+ User=caddy
207
+ Group=www-data
208
+ ExecStart=/usr/bin/caddy run --environ --resume
209
+ TimeoutStopSec=5s
210
+ LimitNOFILE=1048576
211
+ PrivateTmp=true
212
+ ProtectSystem=full
213
+ AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
214
+
215
+ [Install]
216
+ WantedBy=multi-user.target
217
+ """
fujin/proxies/dummy.py CHANGED
@@ -1,16 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
1
5
  from fujin.config import Config
2
- from fujin.host import Host
6
+ from fujin.connection import Connection
3
7
 
4
8
 
5
9
  class WebProxy:
6
- host: Host
7
- config: Config
10
+ config_file: Path
11
+
12
+ @classmethod
13
+ def create(cls, _: Config, __: Connection) -> WebProxy:
14
+ return cls()
8
15
 
9
16
  def install(self):
10
17
  pass
11
18
 
19
+ def uninstall(self):
20
+ pass
21
+
12
22
  def setup(self):
13
23
  pass
14
24
 
15
25
  def teardown(self):
16
26
  pass
27
+
28
+ def export_config(self):
29
+ pass
fujin/proxies/nginx.py CHANGED
@@ -1,59 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from pathlib import Path
5
+
1
6
  import msgspec
2
7
 
3
8
  from fujin.config import Config
4
- from fujin.host import Host
5
-
6
- CERTBOT_EMAIL = ""
9
+ from fujin.connection import Connection
7
10
 
8
- # TODO: this is a wip
11
+ CERTBOT_EMAIL = os.getenv("CERTBOT_EMAIL")
9
12
 
10
13
 
11
14
  class WebProxy(msgspec.Struct):
12
- host: Host
13
- config: Config
15
+ conn: Connection
16
+ domain_name: str
17
+ app_name: str
18
+ upstream: str
19
+ statics: dict[str, str]
20
+ local_config_dir: Path
21
+
22
+ @property
23
+ def config_file(self) -> Path:
24
+ return self.local_config_dir / f"{self.app_name}.conf"
25
+
26
+ @classmethod
27
+ def create(cls, config: Config, conn: Connection) -> WebProxy:
28
+ return cls(
29
+ conn=conn,
30
+ domain_name=config.host.domain_name,
31
+ upstream=config.webserver.upstream,
32
+ app_name=config.app_name,
33
+ local_config_dir=config.local_config_dir,
34
+ statics=config.webserver.statics,
35
+ )
36
+
37
+ def run_pty(self, *args, **kwargs):
38
+ return self.conn.run(*args, **kwargs, pty=True)
14
39
 
15
40
  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"
41
+ self.conn.run(
42
+ "sudo apt install -y nginx libpq-dev python3-dev python3-certbot-nginx"
19
43
  )
20
44
 
45
+ def uninstall(self):
46
+ self.stop()
47
+ self.conn.run("sudo apt remove -y nginx")
48
+ self.conn.run(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
49
+ self.conn.run(f"sudo rm /etc/nginx/sites-enabled/{self.app_name}.conf")
50
+ self.conn.run("sudo systemctl disable certbot.timer")
51
+ self.conn.run("sudo apt remove -y python3-certbot-nginx")
52
+
21
53
  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",
54
+ conf = (
55
+ self.config_file.read_text()
56
+ if self.config_file.exists()
57
+ else self._get_config()
25
58
  )
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"
59
+ self.run_pty(
60
+ f"sudo echo '{conf}' | sudo tee /etc/nginx/sites-available/{self.app_name}.conf",
61
+ hide="out",
31
62
  )
32
- # Updating local Nginx configuration
33
- self.host.get(
34
- f"/etc/nginx/sites-available/{self.config.app}",
35
- f".fujin/{self.config.app}",
63
+ self.run_pty(
64
+ f"sudo ln -sf /etc/nginx/sites-available/{self.app_name}.conf /etc/nginx/sites-enabled/{self.app_name}.conf",
36
65
  )
37
- # Enabling certificate auto-renewal
38
- self.host.sudo("systemctl enable certbot.timer")
39
- self.host.sudo("systemctl start certbot.timer")
66
+ if CERTBOT_EMAIL:
67
+ cert_path = f"/etc/letsencrypt/live/{self.domain_name}/fullchain.pem"
68
+ cert_exists = self.conn.run(f"sudo test -f {cert_path}", warn=True).ok
69
+
70
+ if not cert_exists:
71
+ self.conn.run(
72
+ f"certbot --nginx -d {self.domain_name} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect"
73
+ )
74
+ self.config_file.parent.mkdir(exist_ok=True)
75
+ self.conn.get(
76
+ f"/etc/nginx/sites-available/{self.app_name}.conf",
77
+ str(self.config_file),
78
+ )
79
+ self.run_pty("sudo systemctl enable certbot.timer")
80
+ self.run_pty("sudo systemctl start certbot.timer")
81
+ self.restart()
40
82
 
41
83
  def teardown(self):
42
- pass
84
+ self.run_pty(f"sudo rm /etc/nginx/sites-available/{self.app_name}.conf")
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
+ )
89
+
90
+ def start(self) -> None:
91
+ self.run_pty("sudo systemctl start nginx")
92
+
93
+ def stop(self) -> None:
94
+ self.run_pty("sudo systemctl stop nginx")
95
+
96
+ def status(self) -> None:
97
+ self.run_pty("sudo systemctl status nginx", warn=True)
98
+
99
+ def restart(self) -> None:
100
+ self.run_pty("sudo systemctl restart nginx")
101
+
102
+ def logs(self) -> None:
103
+ self.run_pty(f"sudo journalctl -u nginx -f", warn=True)
104
+
105
+ def export_config(self) -> None:
106
+ self.config_file.write_text(self._get_config())
43
107
 
44
108
  def _get_config(self) -> str:
109
+ static_locations = ""
110
+ for path, directory in self.statics.items():
111
+ static_locations += f"""
112
+ location {path} {{
113
+ alias {directory};
114
+ }}
115
+ """
116
+
45
117
  return f"""
46
118
  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
- }}
119
+ listen 80;
120
+ server_name {self.domain_name};
58
121
 
122
+ {static_locations}
123
+
124
+ location / {{
125
+ proxy_pass {self.upstream};
126
+ proxy_set_header Host $host;
127
+ proxy_set_header X-Real-IP $remote_addr;
128
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
129
+ proxy_set_header X-Forwarded-Proto $scheme;
130
+ }}
131
+ }}
59
132
  """
@@ -0,0 +1,14 @@
1
+ # All options are documented here https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
2
+ [Unit]
3
+ Description={app_name} Worker
4
+
5
+ [Service]
6
+ User={user}
7
+ Group={user}
8
+ WorkingDirectory={app_dir}
9
+ ExecStart={app_dir}/{command}
10
+ EnvironmentFile={app_dir}/.env
11
+ Restart=always
12
+
13
+ [Install]
14
+ WantedBy=multi-user.target
@@ -1,24 +1,25 @@
1
- # TODO: add tons of comments in this file
1
+ # All options are documented here https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
2
+ # Inspiration was taken from here https://docs.gunicorn.org/en/stable/deploy.html#systemd
2
3
  [Unit]
3
- Description={app} daemon
4
- Requires={app}.socket
4
+ Description={app_name} daemon
5
+ Requires={app_name}.socket
5
6
  After=network.target
6
7
 
7
8
  [Service]
8
- Type=notify
9
- NotifyAccess=main
9
+ #Type=notify
10
+ #NotifyAccess=main
10
11
  User={user}
11
- Group=www-data
12
- RuntimeDirectory={app}
13
- WorkingDirectory={project_dir}
14
- ExecStart={project_dir}/{command}
15
- EnvironmentFile={project_dir}/.env
12
+ Group={user}
13
+ RuntimeDirectory={app_name}
14
+ WorkingDirectory={app_dir}
15
+ ExecStart={app_dir}/{command}
16
+ EnvironmentFile={app_dir}/.env
16
17
  ExecReload=/bin/kill -s HUP $MAINPID
17
18
  KillMode=mixed
18
19
  TimeoutStopSec=5
19
20
  PrivateTmp=true
20
21
  # if your app does not need administrative capabilities, let systemd know
21
- # ProtectSystem=strict
22
+ ProtectSystem=strict
22
23
 
23
24
  [Install]
24
25
  WantedBy=multi-user.target
@@ -1,8 +1,8 @@
1
1
  [Unit]
2
- Description={app} socket
2
+ Description={app_name} socket
3
3
 
4
4
  [Socket]
5
- ListenStream=/run/{app}.sock
5
+ ListenStream=/run/{app_name}.sock
6
6
  SocketUser=www-data
7
7
  SocketGroup=www-data
8
8
  SocketMode=0660
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.3
2
+ Name: fujin-cli
3
+ Version: 0.4.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
+ Keywords: caddy,deployment,django,fastapi,litestar,python,systemd
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: cappa>=0.24
24
+ Requires-Dist: fabric>=3.2.2
25
+ Requires-Dist: msgspec[toml]>=0.18.6
26
+ Requires-Dist: rich>=13.9.2
27
+ Description-Content-Type: text/markdown
28
+
29
+ # fujin
30
+
31
+ [![PyPI - Version](https://img.shields.io/pypi/v/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
32
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
33
+ [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/falcopackages/fujin/blob/main/LICENSE.txt)
34
+ [![Status](https://img.shields.io/pypi/status/fujin-cli.svg)](https://pypi.org/project/fujin-cli)
35
+ -----
36
+
37
+ > [!IMPORTANT]
38
+ > This package currently contains minimal features and is a work-in-progress
39
+
40
+ `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
+ 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
+ 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
+ Check out the [documentation📚](https://fujin.readthedocs.io/en/latest/) for installation, features, and usage guides.
45
+
46
+ ## Why?
47
+
48
+ 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 the configuration file simpler 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
+ 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
+ I'm currently rocking SQLite in production for my side projects and ths setup is enough for my use case.
53
+
54
+ The goal is to automate deployment while leaving you in full control of your Linux box. It's not a CLI PaaS - it's simple and expects you to be able to SSH into your server and troubleshoot if necessary. For beginners, it makes the initial deployment easier while you get your hands dirty with Linux.
55
+ If you need a never-break, worry-free, set-it-and-forget-it setup that auto-scales and does all the magic, fujin probably isn't for you.
56
+
57
+ ## Inspiration and alternatives
58
+
59
+ Fujin draws inspiration from the following tools for their developer experience. These are better alternatives if you need a more robust, set-and-forget solution
60
+
61
+ - [fly.io](https://fly.io/)
62
+ - [kamal](https://kamal-deploy.org/) (you probably can't just forget this one)
63
+
64
+ ## License
65
+
66
+ `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,35 @@
1
+ fujin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ fujin/__main__.py,sha256=St0VnEWhRRw_ukAddAwDGFliLqQT3xlone-9JIONlDI,1702
3
+ fujin/config.py,sha256=OYcQPhYYcLJ2ImVP-BNodWvaJ6Y5mRIeB9R3cohbV94,8678
4
+ fujin/connection.py,sha256=ZkYaNykRFj9Yr-K-vOrZtVVGUDurDm6W7OQrgct71CA,2428
5
+ fujin/errors.py,sha256=74Rh-Sgql1YspPdR_akQ2G3xZ48zecyafYCptpaFo1A,73
6
+ fujin/hooks.py,sha256=QHIqxLxujG2U70UkN1BpUplE6tTqn7pFJP5oHde1tUQ,1350
7
+ fujin/commands/__init__.py,sha256=uIGGXt8YofL5RZn8KIy153ioWGoCl32ffHtqOhB-6ZM,78
8
+ fujin/commands/_base.py,sha256=o3R4-c3XeFWTIW3stiUdrcCPwdjzfjUVIpZy2L1-gZ4,2525
9
+ fujin/commands/app.py,sha256=LUC4mmvkp1F4a96UP9hgd5y_R9HA8Xc_TtNAsYtXvvs,5153
10
+ fujin/commands/config.py,sha256=HAiea_ebhvBzUG7vddMH3ldjZKRHU1KFaZlDZI1XmMg,2322
11
+ fujin/commands/deploy.py,sha256=mUvlDF6yv05wnQqhsFC-LCPVvaJVfnQnK97CFB_ttVY,4427
12
+ fujin/commands/docs.py,sha256=b5FZ8AgoAfn4q4BueEQvM2w5HCuh8-rwBqv_CRFVU8E,349
13
+ fujin/commands/down.py,sha256=JrwDZjcqyx5Mx7IzcAkzq1zAPBD5MQMIS4gzBEUz3JM,1721
14
+ fujin/commands/init.py,sha256=MqZeEH7OL4knOuR8D84MxmeurDMXSr8XEXqcGxf6Xoo,2895
15
+ fujin/commands/proxy.py,sha256=w8XOhirD5lLnmW3SC27bahoIt2XtgydQ-8LxOP-DpYM,2552
16
+ fujin/commands/prune.py,sha256=C2aAN6AUS84jgRg1eiCroyiuZyaZDmf5yvGAQY9xkcg,1517
17
+ fujin/commands/redeploy.py,sha256=suSr6S54oLsnKGjJgq3uHxCRFcrCX5C-5BkmH5MzQks,1864
18
+ fujin/commands/rollback.py,sha256=BN9vOTEBcSSpFIfck9nzWvMVO7asVC20lQbcNrxRchg,2009
19
+ fujin/commands/secrets.py,sha256=1xZQVkvbopsAcWUocLstxPKxsvmGoE2jWip5hdTrP50,162
20
+ fujin/commands/server.py,sha256=n0FZbNI4IO5AOMJTIVfJqgb32-QSrWEu0IVwfREsRbY,3637
21
+ fujin/commands/up.py,sha256=DgDN-1mc_mMHJRCIvcB947Cd5a7phunu9NpXloGK0UU,419
22
+ fujin/process_managers/__init__.py,sha256=MhhfTBhm64zWRAKgjvsZRIToOUJus60vGScbAjqpQ6Y,994
23
+ fujin/process_managers/systemd.py,sha256=qG_4Ew8SEWtaTFOAW_XZXsMO2WjFWZ4dp5nBwAPBObk,5603
24
+ fujin/proxies/__init__.py,sha256=UuWYU175tkdaz1WWRCDDpQgGfFVYYNR9PBxA3lTCNr0,695
25
+ fujin/proxies/caddy.py,sha256=OkQkQXlVVw3KYkVNV8tAfpHnqZEImo5bStr8dbkM2F0,7540
26
+ fujin/proxies/dummy.py,sha256=qBKSn8XNEA9SVwB7GzRNX2l9Iw6tUjo2CFqZjWi0FjY,465
27
+ fujin/proxies/nginx.py,sha256=8AkbJAjj6B0fxgv671mGDbx3LY_dY5wxFov80XmSfUY,4139
28
+ fujin/templates/simple.service,sha256=-lyKjmSyfHGucP4O_vRQE1NNaHq0Qjsc0twdwoRLgI0,321
29
+ fujin/templates/web.service,sha256=NZ7ZeaFvV_MZTBn8QqRQeu8PIrWHf3aWYWNzjOQeqCw,685
30
+ fujin/templates/web.socket,sha256=2lJsiOHlMJL0YlN7YBLLnr5zqsytPEt81yP34nk0dmc,173
31
+ fujin_cli-0.4.0.dist-info/METADATA,sha256=Vf2x0v7d1QVV9xE9UQn2LNtDyVCFmASfDcwTcpgpAgU,4396
32
+ fujin_cli-0.4.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
33
+ fujin_cli-0.4.0.dist-info/entry_points.txt,sha256=Y_TBtKt3j11qhwquMexZR5yqnDEqOBDACtresqQFE-s,46
34
+ fujin_cli-0.4.0.dist-info/licenses/LICENSE.txt,sha256=0QF8XfuH0zkIHhSet6teXfiCze6JSdr8inRkmLLTDyo,1099
35
+ fujin_cli-0.4.0.dist-info/RECORD,,