fujin-cli 0.8.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fujin-cli might be problematic. Click here for more details.
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/CHANGELOG.md +10 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/PKG-INFO +1 -1
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/configuration.rst +2 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/pyproject.toml +2 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/config.py +10 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/deploy.py +7 -7
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/init.py +2 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/printenv.py +2 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/redeploy.py +2 -2
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/config.py +23 -16
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/__init__.py +13 -10
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/uv.lock +1 -1
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.github/workflows/publish.yml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.gitignore +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.pre-commit-config.yaml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/.readthedocs.yaml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/LICENSE.txt +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/README.md +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/Vagrantfile +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/changelog.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/app.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/config.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/deploy.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/docs.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/down.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/index.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/init.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/printenv.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/proxy.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/prune.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/redeploy.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/rollback.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/server.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/commands/up.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/conf.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/hooks.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/index.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/installation.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/requirements.txt +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/secrets.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/docs/tutorial.rst +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/README.md +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/__init__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/__main__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/asgi.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/settings.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/urls.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/bookstore/wsgi.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/fujin.toml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/manage.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/pyproject.toml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/django/bookstore/requirements.txt +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/golang/pocketbase/.env.prod +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/examples/golang/pocketbase/fujin.toml +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/justfile +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/__init__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/__main__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/__init__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/_base.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/app.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/docs.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/down.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/proxy.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/prune.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/rollback.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/server.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/commands/up.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/connection.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/errors.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/hooks.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/process_managers/__init__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/process_managers/systemd.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/__init__.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/caddy.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/dummy.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/proxies/nginx.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/bitwarden.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/secrets/onepassword.py +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/simple.service +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/web.service +0 -0
- {fujin_cli-0.8.0 → fujin_cli-0.9.0}/src/fujin/templates/web.socket +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.9.0] - 2024-11-23
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- Env content can be define directly in toml
|
|
12
|
+
|
|
13
|
+
### 🚜 Refactor
|
|
14
|
+
|
|
15
|
+
- Avoid running secret adapter if no secret placeholder is found
|
|
16
|
+
|
|
7
17
|
## [0.8.0] - 2024-11-23
|
|
8
18
|
|
|
9
19
|
### 🚀 Features
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: fujin-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Get your project up and running in a few minutes on your own vps.
|
|
5
5
|
Project-URL: Documentation, https://github.com/falcopackages/fujin#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/falcopackages/fujin/issues
|
|
@@ -20,7 +20,7 @@ This is a minimal working example.
|
|
|
20
20
|
from fujin.commands.init import simple_config
|
|
21
21
|
from tomli_w import dumps
|
|
22
22
|
|
|
23
|
-
print(dumps(simple_config("bookstore")))
|
|
23
|
+
print(dumps(simple_config("bookstore"), multiline_strings=True))
|
|
24
24
|
#hide:toggle
|
|
25
25
|
|
|
26
26
|
.. tab-item:: binary mode
|
|
@@ -32,5 +32,5 @@ This is a minimal working example.
|
|
|
32
32
|
from fujin.commands.init import binary_config
|
|
33
33
|
from tomli_w import dumps
|
|
34
34
|
|
|
35
|
-
print(dumps(binary_config("bookstore")))
|
|
35
|
+
print(dumps(binary_config("bookstore"), multiline_strings=True))
|
|
36
36
|
#hide:toggle
|
|
@@ -7,7 +7,7 @@ requires = [
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "fujin-cli"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.9.0"
|
|
11
11
|
description = "Get your project up and running in a few minutes on your own vps."
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
keywords = [
|
|
@@ -154,7 +154,7 @@ lint.isort.required-imports = [
|
|
|
154
154
|
lint.pyupgrade.keep-runtime-typing = true
|
|
155
155
|
|
|
156
156
|
[tool.bumpversion]
|
|
157
|
-
current_version = "0.
|
|
157
|
+
current_version = "0.9.0"
|
|
158
158
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
159
159
|
serialize = [
|
|
160
160
|
"{major}.{minor}.{patch}",
|
|
@@ -37,9 +37,17 @@ class ConfigCMD(BaseCommand):
|
|
|
37
37
|
)
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
+
host_config = {
|
|
41
|
+
f: getattr(self.config.host, f) for f in self.config.host.__struct_fields__
|
|
42
|
+
}
|
|
43
|
+
host_config.pop("_key_filename")
|
|
44
|
+
host_config.pop("_env_file")
|
|
45
|
+
host_config.pop("env_content")
|
|
46
|
+
if self.config.host.key_filename:
|
|
47
|
+
host_config["key_filename"] = self.config.host.key_filename
|
|
48
|
+
host_config["env_content"] = self.config.host.env_content
|
|
40
49
|
host_config_text = "\n".join(
|
|
41
|
-
f"[dim]{key}:[/dim] {value}"
|
|
42
|
-
for key, value in self.config.host.to_dict().items()
|
|
50
|
+
f"[dim]{key}:[/dim] {value}" for key, value in host_config.items()
|
|
43
51
|
)
|
|
44
52
|
console.print(
|
|
45
53
|
Panel(
|
|
@@ -52,15 +52,15 @@ class Deploy(BaseCommand):
|
|
|
52
52
|
return f"{self.app_dir}/v{self.config.version}"
|
|
53
53
|
|
|
54
54
|
def parse_envfile(self) -> str:
|
|
55
|
-
if not self.config.host.envfile.exists():
|
|
56
|
-
raise cappa.Exit(f"{self.config.host.envfile} not found", code=1)
|
|
57
55
|
if self.config.secret_config:
|
|
58
56
|
self.stdout.output("[blue]Reading secrets....[/blue]")
|
|
59
|
-
return resolve_secrets(
|
|
60
|
-
|
|
57
|
+
return resolve_secrets(
|
|
58
|
+
self.config.host.env_content, self.config.secret_config
|
|
59
|
+
)
|
|
60
|
+
return self.config.host.env_content
|
|
61
61
|
|
|
62
62
|
def transfer_files(
|
|
63
|
-
|
|
63
|
+
self, conn: Connection, env: str, skip_requirements: bool = False
|
|
64
64
|
):
|
|
65
65
|
conn.run(f"echo '{env}' > {self.app_dir}/.env")
|
|
66
66
|
distfile_path = self.config.get_distfile_path()
|
|
@@ -78,7 +78,7 @@ class Deploy(BaseCommand):
|
|
|
78
78
|
)
|
|
79
79
|
|
|
80
80
|
def install_project(
|
|
81
|
-
|
|
81
|
+
self, conn: Connection, version: str | None = None, *, skip_setup: bool = False
|
|
82
82
|
):
|
|
83
83
|
version = version or self.config.version
|
|
84
84
|
if self.config.installation_mode == InstallationMode.PY_PACKAGE:
|
|
@@ -87,7 +87,7 @@ class Deploy(BaseCommand):
|
|
|
87
87
|
self._install_binary(conn, version)
|
|
88
88
|
|
|
89
89
|
def _install_python_package(
|
|
90
|
-
|
|
90
|
+
self, conn: Connection, version: str, skip_setup: bool = False
|
|
91
91
|
):
|
|
92
92
|
appenv = f"""
|
|
93
93
|
set -a # Automatically export all variables
|
|
@@ -31,7 +31,7 @@ class Init(BaseCommand):
|
|
|
31
31
|
}
|
|
32
32
|
app_name = Path().resolve().stem.replace("-", "_").replace(" ", "_").lower()
|
|
33
33
|
config = profile_to_func[self.profile](app_name)
|
|
34
|
-
fujin_toml.write_text(tomli_w.dumps(config))
|
|
34
|
+
fujin_toml.write_text(tomli_w.dumps(config, multiline_strings=True))
|
|
35
35
|
self.stdout.output(
|
|
36
36
|
"[green]Sample configuration file generated successfully![/green]"
|
|
37
37
|
)
|
|
@@ -57,7 +57,7 @@ def simple_config(app_name) -> dict:
|
|
|
57
57
|
"host": {
|
|
58
58
|
"user": "root",
|
|
59
59
|
"domain_name": f"{app_name}.com",
|
|
60
|
-
"
|
|
60
|
+
"env_content": f"DEBUG=False\nALLOWED_HOSTS={app_name}.com\n",
|
|
61
61
|
},
|
|
62
62
|
}
|
|
63
63
|
if not Path(".python-version").exists():
|
|
@@ -11,8 +11,8 @@ class Printenv(BaseCommand):
|
|
|
11
11
|
def __call__(self):
|
|
12
12
|
if self.config.secret_config:
|
|
13
13
|
result = resolve_secrets(
|
|
14
|
-
self.config.host.
|
|
14
|
+
self.config.host.env_content, self.config.secret_config
|
|
15
15
|
)
|
|
16
16
|
else:
|
|
17
|
-
result = self.config.host.
|
|
17
|
+
result = self.config.host.env_content
|
|
18
18
|
self.stdout.output(result)
|
|
@@ -34,8 +34,8 @@ class Redeploy(BaseCommand):
|
|
|
34
34
|
|
|
35
35
|
def _copy_requirements_if_needed(self, conn: Connection) -> bool:
|
|
36
36
|
if (
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
not self.config.requirements
|
|
38
|
+
or self.config.installation_mode == InstallationMode.BINARY
|
|
39
39
|
):
|
|
40
40
|
return False
|
|
41
41
|
local_requirements = hashlib.md5(
|
|
@@ -136,10 +136,18 @@ The login user for running remote tasks. Should have passwordless sudo access fo
|
|
|
136
136
|
|
|
137
137
|
You can create a user with these requirements using the ``fujin server create-user`` command.
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
env_file
|
|
140
|
+
~~~~~~~~
|
|
141
141
|
Path to the production environment file that will be copied to the host.
|
|
142
142
|
|
|
143
|
+
env_content
|
|
144
|
+
~~~~~~~~~~~
|
|
145
|
+
A string containing the production environment variables, ideal for scenarios where most variables are retrieved from secrets and you prefer not to use a separate file.
|
|
146
|
+
|
|
147
|
+
.. important::
|
|
148
|
+
|
|
149
|
+
``env_file`` and ``env_content`` are mutually exclusive—you can define only one.
|
|
150
|
+
|
|
143
151
|
apps_dir
|
|
144
152
|
~~~~~~~~
|
|
145
153
|
|
|
@@ -178,7 +186,7 @@ Example:
|
|
|
178
186
|
|
|
179
187
|
hooks
|
|
180
188
|
-----
|
|
181
|
-
Run custom scripts at specific points with hooks. Check out the `
|
|
189
|
+
Run custom scripts at specific points with hooks. Check out the `hooks </hooks.html>`_ page for more information.
|
|
182
190
|
|
|
183
191
|
"""
|
|
184
192
|
|
|
@@ -271,28 +279,27 @@ class HostConfig(msgspec.Struct, kw_only=True):
|
|
|
271
279
|
ip: str | None = None
|
|
272
280
|
domain_name: str
|
|
273
281
|
user: str
|
|
274
|
-
|
|
282
|
+
_env_file: str = msgspec.field(name="envfile", default="")
|
|
283
|
+
env_content: str = ""
|
|
275
284
|
apps_dir: str = ".local/share/fujin"
|
|
276
285
|
password_env: str | None = None
|
|
277
286
|
ssh_port: int = 22
|
|
278
287
|
_key_filename: str | None = msgspec.field(name="key_filename", default=None)
|
|
279
288
|
|
|
280
289
|
def __post_init__(self):
|
|
290
|
+
if self._env_file and self.env_content:
|
|
291
|
+
raise ImproperlyConfiguredError(
|
|
292
|
+
"Cannot set both 'env_content' and 'env_file' properties."
|
|
293
|
+
)
|
|
294
|
+
if not self.env_content:
|
|
295
|
+
envfile = Path(self._env_file)
|
|
296
|
+
if not envfile.exists():
|
|
297
|
+
raise ImproperlyConfiguredError(f"{self._env_file} not found")
|
|
298
|
+
self.env_content = envfile.read_text()
|
|
299
|
+
self.env_content = self.env_content.strip()
|
|
281
300
|
self.apps_dir = f"/home/{self.user}/{self.apps_dir}"
|
|
282
301
|
self.ip = self.ip or self.domain_name
|
|
283
302
|
|
|
284
|
-
def to_dict(self):
|
|
285
|
-
d = {f: getattr(self, f) for f in self.__struct_fields__}
|
|
286
|
-
d.pop("_key_filename")
|
|
287
|
-
d.pop("_envfile")
|
|
288
|
-
d["key_filename"] = self.key_filename
|
|
289
|
-
d["envfile"] = self.envfile
|
|
290
|
-
return d
|
|
291
|
-
|
|
292
|
-
@property
|
|
293
|
-
def envfile(self) -> Path:
|
|
294
|
-
return Path(self._envfile)
|
|
295
|
-
|
|
296
303
|
@property
|
|
297
304
|
def key_filename(self) -> Path | None:
|
|
298
305
|
if self._key_filename:
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from contextlib import closing
|
|
4
|
+
from io import StringIO
|
|
5
|
+
from typing import Callable, ContextManager
|
|
5
6
|
|
|
6
7
|
import gevent
|
|
7
8
|
from dotenv import dotenv_values
|
|
8
9
|
|
|
9
|
-
from .bitwarden import bitwarden
|
|
10
|
-
from .onepassword import one_password
|
|
11
10
|
from fujin.config import SecretAdapter
|
|
12
11
|
from fujin.config import SecretConfig
|
|
13
|
-
|
|
12
|
+
from .bitwarden import bitwarden
|
|
13
|
+
from .onepassword import one_password
|
|
14
14
|
|
|
15
15
|
secret_reader = Callable[[str], str]
|
|
16
|
-
secret_adapter_context = Callable[[SecretConfig], secret_reader]
|
|
16
|
+
secret_adapter_context = Callable[[SecretConfig], ContextManager[secret_reader]]
|
|
17
17
|
|
|
18
18
|
adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
|
|
19
19
|
SecretAdapter.BITWARDEN: bitwarden,
|
|
@@ -21,14 +21,17 @@ adapter_to_context: dict[SecretAdapter, secret_adapter_context] = {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
def resolve_secrets(
|
|
25
|
-
|
|
24
|
+
def resolve_secrets(env_content: str, secret_config: SecretConfig) -> str:
|
|
25
|
+
with closing(StringIO(env_content)) as buffer:
|
|
26
|
+
env_dict = dotenv_values(stream=buffer)
|
|
26
27
|
secrets = {key: value for key, value in env_dict.items() if value.startswith("$")}
|
|
28
|
+
if not secrets:
|
|
29
|
+
return env_content
|
|
27
30
|
adapter_context = adapter_to_context[secret_config.adapter]
|
|
28
31
|
parsed_secrets = {}
|
|
29
|
-
with adapter_context(secret_config) as
|
|
32
|
+
with adapter_context(secret_config) as reader:
|
|
30
33
|
for key, secret in secrets.items():
|
|
31
|
-
parsed_secrets[key] = gevent.spawn(
|
|
34
|
+
parsed_secrets[key] = gevent.spawn(reader, secret[1:])
|
|
32
35
|
gevent.joinall(parsed_secrets.values())
|
|
33
36
|
env_dict.update({key: thread.value for key, thread in parsed_secrets.items()})
|
|
34
37
|
return "\n".join(f'{key}="{value}"' for key, value in env_dict.items())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|