fujin-cli 0.1.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.

@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Tobi DEGNON <tobidegnon@proton.me>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,41 @@
1
+ Metadata-Version: 2.3
2
+ Name: fujin-cli
3
+ Version: 0.1.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
+ Requires-Python: >=3.10
11
+ Requires-Dist: fabric
12
+ Requires-Dist: rich
13
+ Requires-Dist: tomlkit
14
+ Description-Content-Type: text/markdown
15
+
16
+ # fujin
17
+
18
+ [![PyPI - Version](https://img.shields.io/pypi/v/fujin.svg)](https://pypi.org/project/fujin)
19
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin.svg)](https://pypi.org/project/fujin)
20
+
21
+ -----
22
+
23
+ > [!IMPORTANT]
24
+ > This package currently contains no features and is a work-in-progress
25
+
26
+
27
+ **Table of Contents**
28
+
29
+ - [fujin](#fujin)
30
+ - [Installation](#installation)
31
+ - [License](#license)
32
+
33
+ ## Installation
34
+
35
+ ```console
36
+ pip install fujin
37
+ ```
38
+
39
+ ## License
40
+
41
+ `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,26 @@
1
+ # fujin
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/fujin.svg)](https://pypi.org/project/fujin)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fujin.svg)](https://pypi.org/project/fujin)
5
+
6
+ -----
7
+
8
+ > [!IMPORTANT]
9
+ > This package currently contains no features and is a work-in-progress
10
+
11
+
12
+ **Table of Contents**
13
+
14
+ - [fujin](#fujin)
15
+ - [Installation](#installation)
16
+ - [License](#license)
17
+
18
+ ## Installation
19
+
20
+ ```console
21
+ pip install fujin
22
+ ```
23
+
24
+ ## License
25
+
26
+ `fujin` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "fujin-cli"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Tobi DEGNON", email = "tobidegnon@proton.me" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = ["fabric", "rich", "tomlkit"]
11
+
12
+ urls.Documentation = "https://github.com/falcopackages/fujin#readme"
13
+ urls.Issues = "https://github.com/falcopackages/fujin/issues"
14
+ urls.Source = "https://github.com/falcopackages/fujin"
15
+
16
+ [project.scripts]
17
+ fujin = "fujin:main"
18
+
19
+ [build-system]
20
+ requires = ["hatchling"]
21
+ build-backend = "hatchling.build"
22
+
23
+ [tool.hatch.build.targets.wheel]
24
+ packages = [
25
+ "src/fujin",
26
+ ]
@@ -0,0 +1,2 @@
1
+ def main() -> None:
2
+ print("Hello from fujin!")
@@ -0,0 +1,202 @@
1
+ import os
2
+ import secrets
3
+
4
+ from fabric import Connection
5
+ from fabric import task
6
+ from falco.fabutils import curl_binary_download_cmd
7
+ from falco.fabutils import with_progress
8
+
9
+ # Configuration
10
+ PROJECT_NAME = "torch"
11
+ GITHUB_USERNAME = os.getenv("GITHUB_USERNAME", default="tobi")
12
+ GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
13
+ SERVER_USER = "tobi"
14
+ SERVER_HOST = os.getenv("SERVER_HOST")
15
+ SERVER_PASSWORD = os.getenv("SERVER_PASSWORD")
16
+ SERVER_PROJECT_DIR = f"/home/{SERVER_USER}/djangoprojects/{PROJECT_NAME}"
17
+ SERVER_PROJECT_BINARY = f"{SERVER_PROJECT_DIR}/{PROJECT_NAME}"
18
+ DOMAIN = "torch.com"
19
+ CERTBOT_EMAIL = "tobidegnon@proton.me"
20
+
21
+
22
+ def get_connection(use_root=False):
23
+ """Establish a connection to the server."""
24
+ user = "root" if use_root else SERVER_USER
25
+ return Connection(host=SERVER_HOST, user=user, connect_kwargs={"password": SERVER_PASSWORD})
26
+
27
+
28
+ # Deployment Tasks
29
+
30
+
31
+ @task
32
+ @with_progress("Provisioning server")
33
+ def provision(_, set_password=False, progress=None):
34
+ """Set up the server with necessary dependencies and create a new user."""
35
+ conn = get_connection(use_root=True)
36
+
37
+ progress("Updating system and installing dependencies")
38
+ conn.sudo("apt update && apt upgrade -y")
39
+ conn.sudo("apt install -y nginx libpq-dev python3-dev python3-certbot-nginx sqlite3")
40
+
41
+ progress(f"Creating and configuring user: {SERVER_USER}")
42
+ conn.sudo(f"adduser --disabled-password --gecos '' {SERVER_USER}")
43
+ if set_password:
44
+ password = secrets.token_urlsafe(16)
45
+ conn.sudo(f"echo '{SERVER_USER}:{password}' | chpasswd")
46
+ print(f"Generated password for {SERVER_USER}: {password}")
47
+ print("Please store this password securely and change it upon first login.")
48
+
49
+ # Set up SSH and sudo access
50
+ conn.sudo(f"mkdir -p /home/{SERVER_USER}/.ssh")
51
+ conn.sudo(f"cp ~/.ssh/authorized_keys /home/{SERVER_USER}/.ssh/")
52
+ conn.sudo(f"chown -R {SERVER_USER}:{SERVER_USER} /home/{SERVER_USER}/.ssh")
53
+ conn.sudo(f"chmod 700 /home/{SERVER_USER}/.ssh")
54
+ conn.sudo(f"chmod 600 /home/{SERVER_USER}/.ssh/authorized_keys")
55
+ conn.sudo(f"echo '{SERVER_USER} ALL=(ALL) NOPASSWD:ALL' | sudo tee -a /etc/sudoers")
56
+
57
+ progress("Creating project directory")
58
+ conn.run(f"mkdir -p {SERVER_PROJECT_DIR}")
59
+
60
+
61
+ @task
62
+ @with_progress("Performing initial deployment")
63
+ def up(c, progress=None):
64
+ """Perform the initial deployment of the project."""
65
+ conn = get_connection()
66
+
67
+ progress("Transferring files to server")
68
+ transfer_files(c)
69
+ conn.sudo(f"ln -sf /etc/nginx/sites-available/{PROJECT_NAME} /etc/nginx/sites-enabled/{PROJECT_NAME}")
70
+
71
+ progress("Setting up project and services")
72
+ apprun(c, "setup")
73
+ reload_services(c)
74
+ conn.sudo(f"systemctl enable --now {PROJECT_NAME}.socket")
75
+
76
+
77
+ @task
78
+ @with_progress("Deploying code changes")
79
+ def deploy(c, progress=None):
80
+ """Deploy code changes to the server."""
81
+ progress("Transferring updated files")
82
+ transfer_files(c, code_only=True)
83
+
84
+ progress("Updating project setup")
85
+ apprun(c, "setup")
86
+
87
+ progress("Restarting wsgi service")
88
+ get_connection().sudo(f"systemctl restart {PROJECT_NAME}")
89
+
90
+
91
+ # Maintenance Tasks
92
+
93
+
94
+ @task
95
+ @with_progress("Refreshing application")
96
+ def refresh(c, progress=None):
97
+ """Refresh the application by reloading configurations and restarting services."""
98
+ progress("Updating all files")
99
+ transfer_files(c)
100
+
101
+ progress("Reloading configurations and restarting services")
102
+ reload_services(c)
103
+
104
+
105
+ @task
106
+ @with_progress("Setting up domain with SSL")
107
+ def setup_domain(_, progress=None):
108
+ """Set up a domain with SSL using Certbot."""
109
+ conn = get_connection()
110
+
111
+ progress("Obtaining SSL certificate")
112
+ conn.sudo(f"certbot --nginx -d {DOMAIN} --non-interactive --agree-tos --email {CERTBOT_EMAIL} --redirect")
113
+
114
+ progress("Updating local Nginx configuration")
115
+ conn.get(
116
+ f"/etc/nginx/sites-available/{PROJECT_NAME}",
117
+ f"deploy/etc/nginx/sites-available/{PROJECT_NAME}",
118
+ )
119
+
120
+ progress("Enabling certificate auto-renewal")
121
+ conn.sudo("systemctl enable certbot.timer")
122
+ conn.sudo("systemctl start certbot.timer")
123
+
124
+
125
+ # Utility Tasks
126
+
127
+
128
+ @task
129
+ def transfer_files(_, code_only=False):
130
+ """Transfer project files to the server."""
131
+ conn = get_connection()
132
+
133
+ # Download and set up binary
134
+ binary_download_cmd = curl_binary_download_cmd(owner=GITHUB_USERNAME, repo=PROJECT_NAME, token=GITHUB_TOKEN)
135
+ conn.run(f"{binary_download_cmd} -o {SERVER_PROJECT_BINARY}")
136
+ conn.run(f"chmod +x {SERVER_PROJECT_BINARY}")
137
+ conn.put(".env.prod", f"{SERVER_PROJECT_DIR}/.env")
138
+
139
+ if not code_only:
140
+ # Transfer configuration files
141
+ config_files = [
142
+ (
143
+ f"deploy/etc/nginx/sites-available/{PROJECT_NAME}",
144
+ f"/etc/nginx/sites-available/{PROJECT_NAME}",
145
+ ),
146
+ (
147
+ f"deploy/etc/systemd/system/{PROJECT_NAME}.socket",
148
+ f"/etc/systemd/system/{PROJECT_NAME}.socket",
149
+ ),
150
+ (
151
+ f"deploy/etc/systemd/system/{PROJECT_NAME}.service",
152
+ f"/etc/systemd/system/{PROJECT_NAME}.service",
153
+ ),
154
+ ]
155
+ for src, dest in config_files:
156
+ filename = os.path.basename(src)
157
+ conn.put(src, f"{SERVER_PROJECT_DIR}/{filename}")
158
+ conn.sudo(f"mv {SERVER_PROJECT_DIR}/{filename} {dest}")
159
+
160
+
161
+ @task
162
+ def apprun(_, cmd, pty=False):
163
+ """Run a command in the application environment."""
164
+ with get_connection().cd(SERVER_PROJECT_DIR):
165
+ get_connection().run(f"source .env && ./{PROJECT_NAME} '{cmd}'", pty=pty)
166
+
167
+
168
+ @task
169
+ def console(c, shell_type="bash"):
170
+ """Open an interactive shell on the server."""
171
+ conn = get_connection()
172
+ if shell_type == "python":
173
+ apprun(c, "manage shell_plus", pty=True)
174
+ elif shell_type == "db":
175
+ apprun(c, "manage dbshell", pty=True)
176
+ else:
177
+ conn.run("bash", pty=True)
178
+
179
+
180
+ @task
181
+ def reload_services(_):
182
+ """Reload Nginx and systemd configurations, and restart the project service."""
183
+ conn = get_connection()
184
+ conn.sudo("systemctl daemon-reload")
185
+ conn.sudo("systemctl reload nginx")
186
+ conn.sudo(f"systemctl restart {PROJECT_NAME}")
187
+
188
+
189
+ @task
190
+ def logs(_, follow=False):
191
+ """View the last n lines of the project's log file."""
192
+ get_connection().sudo(f"sudo journalctl -u {PROJECT_NAME} -r {'-f' if follow else ''}")
193
+
194
+
195
+ if __name__ == "__main__":
196
+ from fabric.main import Program, Collection
197
+
198
+ ns = Collection(
199
+ provision, up, deploy, refresh, setup_domain, transfer_files, apprun, console, reload_services, logs
200
+ )
201
+ program = Program(namespace=ns)
202
+ program.run()
@@ -0,0 +1,60 @@
1
+ import json
2
+ import urllib.request
3
+ from functools import wraps
4
+
5
+ from rich.progress import Progress
6
+ from rich.progress import SpinnerColumn
7
+ from rich.progress import TextColumn
8
+
9
+
10
+ def with_progress(description, pass_progress=True):
11
+ def decorator(func):
12
+ @wraps(func)
13
+ def wrapper(*args, **kwargs):
14
+ with Progress(
15
+ SpinnerColumn(),
16
+ TextColumn("[cyan]{task.description}"),
17
+ transient=True,
18
+ ) as progress:
19
+ task = progress.add_task(description, total=None)
20
+ if pass_progress:
21
+ kwargs["progress"] = lambda desc: progress.update(task, description=desc)
22
+ result = func(*args, **kwargs)
23
+ progress.update(task, completed=True)
24
+ progress.console.print(f"[green]{description} completed!")
25
+ return result
26
+
27
+ return wrapper
28
+
29
+ return decorator
30
+
31
+
32
+ def _get_asset_id(token, owner, repo, project_name):
33
+ url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest"
34
+ headers = {
35
+ "Accept": "application/vnd.github+json",
36
+ "Authorization": f"Bearer {token}",
37
+ "X-GitHub-Api-Version": "2022-11-28",
38
+ }
39
+ req = urllib.request.Request(url, headers=headers)
40
+ with urllib.request.urlopen(req) as response:
41
+ data = json.loads(response.read().decode())
42
+ assets = data["assets"]
43
+ for asset in assets:
44
+ if asset["name"] == f"{project_name}-x86_64-linux":
45
+ return asset["id"]
46
+ msg = f"Asset not found with name {project_name}-x86_64-linux"
47
+ raise Exception(msg)
48
+
49
+
50
+ def curl_binary_download_cmd(*, owner, repo, project_name=None, token=None):
51
+ curl = "curl -L -H 'Accept: application/octet-stream' "
52
+ project_name = project_name or repo
53
+ if not token:
54
+ return f"{curl} https://github.com/{owner}/{repo}/releases/latest/download/{project_name}-x86_64-linux"
55
+ asset_id = _get_asset_id(token, owner, repo, project_name)
56
+ return (
57
+ f"{curl} -H 'Authorization: Bearer {token}' "
58
+ f"-H 'X-GitHub-Api-Version: 2022-11-28' "
59
+ f"https://api.github.com/repos/{owner}/{repo}/releases/assets/{asset_id}"
60
+ )