plash-cli 0.2.2__tar.gz → 0.3.1__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.
- {plash_cli-0.2.2 → plash_cli-0.3.1}/MANIFEST.in +1 -0
- {plash_cli-0.2.2/plash_cli.egg-info → plash_cli-0.3.1}/PKG-INFO +13 -5
- {plash_cli-0.2.2 → plash_cli-0.3.1}/README.md +9 -4
- plash_cli-0.3.1/plash_cli/__init__.py +1 -0
- plash_cli-0.3.1/plash_cli/_modidx.py +32 -0
- plash_cli-0.3.1/plash_cli/assets/es256_public_key.pem +4 -0
- plash_cli-0.3.1/plash_cli/auth.py +60 -0
- plash_cli-0.2.2/plash_cli/core.py → plash_cli-0.3.1/plash_cli/cli.py +68 -72
- {plash_cli-0.2.2 → plash_cli-0.3.1/plash_cli.egg-info}/PKG-INFO +13 -5
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli.egg-info/SOURCES.txt +4 -2
- plash_cli-0.3.1/plash_cli.egg-info/entry_points.txt +13 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli.egg-info/requires.txt +3 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/settings.ini +11 -11
- plash_cli-0.2.2/plash_cli/__init__.py +0 -1
- plash_cli-0.2.2/plash_cli/_modidx.py +0 -26
- plash_cli-0.2.2/plash_cli.egg-info/entry_points.txt +0 -13
- {plash_cli-0.2.2 → plash_cli-0.3.1}/LICENSE +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli/_bash_magic.py +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli.egg-info/dependency_links.txt +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli.egg-info/not-zip-safe +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/plash_cli.egg-info/top_level.txt +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/pyproject.toml +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/setup.cfg +0 -0
- {plash_cli-0.2.2 → plash_cli-0.3.1}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plash_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI for the Plash hosting service
|
|
5
5
|
Home-page: https://github.com/AnswerDotAI/plash_cli
|
|
6
6
|
Author: Jeremy Howard
|
|
@@ -18,6 +18,9 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: fastcore
|
|
20
20
|
Requires-Dist: httpx>=0.28.1
|
|
21
|
+
Requires-Dist: python-dotenv
|
|
22
|
+
Requires-Dist: pyjwt
|
|
23
|
+
Requires-Dist: cryptography
|
|
21
24
|
Provides-Extra: dev
|
|
22
25
|
Requires-Dist: bash_kernel; extra == "dev"
|
|
23
26
|
Requires-Dist: nbdev; extra == "dev"
|
|
@@ -185,13 +188,18 @@ When you visit that page you should see “Hello, World!”
|
|
|
185
188
|
Learn more about what Plash has to offer in the rest of the docs at:
|
|
186
189
|
<https://docs.pla.sh>
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
looking at the official docs at: <https://fastht.ml/docs/>.
|
|
190
|
-
|
|
191
|
-
Particularly, we recommend the following:
|
|
191
|
+
To learn more about creating web apps with FastHTML, we recommend
|
|
192
|
+
looking at the official docs at: <https://fastht.ml/docs/>. In
|
|
193
|
+
particular, we recommend the following:
|
|
192
194
|
|
|
193
195
|
1. [OAuth](https://fastht.ml/docs/explains/oauth.html) - Setup
|
|
194
196
|
authentication for your Plash App with Google Sign-In or other OAuth
|
|
195
197
|
Providers.
|
|
196
198
|
2. [Stripe](https://fastht.ml/docs/explains/stripe.html) - Accept
|
|
197
199
|
payments for your products hosted on Plash with Stripe.
|
|
200
|
+
|
|
201
|
+
Would you like to use a different python web framework? Then check out
|
|
202
|
+
the [examples
|
|
203
|
+
section](https://github.com/AnswerDotAI/plash_cli/tree/main/examples) of
|
|
204
|
+
the repo, it has examples for many popular python web frameworks like
|
|
205
|
+
FastAPI, Gradio, and Streamlit.
|
|
@@ -148,13 +148,18 @@ When you visit that page you should see “Hello, World!”
|
|
|
148
148
|
Learn more about what Plash has to offer in the rest of the docs at:
|
|
149
149
|
<https://docs.pla.sh>
|
|
150
150
|
|
|
151
|
-
|
|
152
|
-
looking at the official docs at: <https://fastht.ml/docs/>.
|
|
153
|
-
|
|
154
|
-
Particularly, we recommend the following:
|
|
151
|
+
To learn more about creating web apps with FastHTML, we recommend
|
|
152
|
+
looking at the official docs at: <https://fastht.ml/docs/>. In
|
|
153
|
+
particular, we recommend the following:
|
|
155
154
|
|
|
156
155
|
1. [OAuth](https://fastht.ml/docs/explains/oauth.html) - Setup
|
|
157
156
|
authentication for your Plash App with Google Sign-In or other OAuth
|
|
158
157
|
Providers.
|
|
159
158
|
2. [Stripe](https://fastht.ml/docs/explains/stripe.html) - Accept
|
|
160
159
|
payments for your products hosted on Plash with Stripe.
|
|
160
|
+
|
|
161
|
+
Would you like to use a different python web framework? Then check out
|
|
162
|
+
the [examples
|
|
163
|
+
section](https://github.com/AnswerDotAI/plash_cli/tree/main/examples) of
|
|
164
|
+
the repo, it has examples for many popular python web frameworks like
|
|
165
|
+
FastAPI, Gradio, and Streamlit.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.1"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Autogenerated by nbdev
|
|
2
|
+
|
|
3
|
+
d = { 'settings': { 'branch': 'main',
|
|
4
|
+
'doc_baseurl': '/plash_cli',
|
|
5
|
+
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
|
+
'git_url': 'https://github.com/AnswerDotAI/plash_cli',
|
|
7
|
+
'lib_path': 'plash_cli'},
|
|
8
|
+
'syms': { 'plash_cli.auth': { 'plash_cli.auth.PlashAuthError': ('auth.html#plashautherror', 'plash_cli/auth.py'),
|
|
9
|
+
'plash_cli.auth._parse_jwt': ('auth.html#_parse_jwt', 'plash_cli/auth.py'),
|
|
10
|
+
'plash_cli.auth._signin_url': ('auth.html#_signin_url', 'plash_cli/auth.py'),
|
|
11
|
+
'plash_cli.auth.goog_id_from_signin_reply': ('auth.html#goog_id_from_signin_reply', 'plash_cli/auth.py'),
|
|
12
|
+
'plash_cli.auth.mk_signin_url': ('auth.html#mk_signin_url', 'plash_cli/auth.py')},
|
|
13
|
+
'plash_cli.cli': { 'plash_cli.cli.PlashError': ('cli.html#plasherror', 'plash_cli/cli.py'),
|
|
14
|
+
'plash_cli.cli._deps': ('cli.html#_deps', 'plash_cli/cli.py'),
|
|
15
|
+
'plash_cli.cli._endpoint': ('cli.html#_endpoint', 'plash_cli/cli.py'),
|
|
16
|
+
'plash_cli.cli._gen_app_name': ('cli.html#_gen_app_name', 'plash_cli/cli.py'),
|
|
17
|
+
'plash_cli.cli._get_app_name': ('cli.html#_get_app_name', 'plash_cli/cli.py'),
|
|
18
|
+
'plash_cli.cli._get_client': ('cli.html#_get_client', 'plash_cli/cli.py'),
|
|
19
|
+
'plash_cli.cli._is_included': ('cli.html#_is_included', 'plash_cli/cli.py'),
|
|
20
|
+
'plash_cli.cli._mk_auth_req': ('cli.html#_mk_auth_req', 'plash_cli/cli.py'),
|
|
21
|
+
'plash_cli.cli._poll_cookies': ('cli.html#_poll_cookies', 'plash_cli/cli.py'),
|
|
22
|
+
'plash_cli.cli._validate_app': ('cli.html#_validate_app', 'plash_cli/cli.py'),
|
|
23
|
+
'plash_cli.cli.apps': ('cli.html#apps', 'plash_cli/cli.py'),
|
|
24
|
+
'plash_cli.cli.create_tar_archive': ('cli.html#create_tar_archive', 'plash_cli/cli.py'),
|
|
25
|
+
'plash_cli.cli.delete': ('cli.html#delete', 'plash_cli/cli.py'),
|
|
26
|
+
'plash_cli.cli.deploy': ('cli.html#deploy', 'plash_cli/cli.py'),
|
|
27
|
+
'plash_cli.cli.download': ('cli.html#download', 'plash_cli/cli.py'),
|
|
28
|
+
'plash_cli.cli.login': ('cli.html#login', 'plash_cli/cli.py'),
|
|
29
|
+
'plash_cli.cli.logs': ('cli.html#logs', 'plash_cli/cli.py'),
|
|
30
|
+
'plash_cli.cli.start': ('cli.html#start', 'plash_cli/cli.py'),
|
|
31
|
+
'plash_cli.cli.stop': ('cli.html#stop', 'plash_cli/cli.py'),
|
|
32
|
+
'plash_cli.cli.view': ('cli.html#view', 'plash_cli/cli.py')}}}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Client side logic to add Plash Auth to your app"""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_auth.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto 0
|
|
6
|
+
__all__ = ['signin_completed_rt', 'mk_signin_url', 'PlashAuthError', 'goog_id_from_signin_reply']
|
|
7
|
+
|
|
8
|
+
# %% ../nbs/01_auth.ipynb 4
|
|
9
|
+
import httpx,os,jwt
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from warnings import warn
|
|
12
|
+
|
|
13
|
+
from . import __version__
|
|
14
|
+
|
|
15
|
+
# %% ../nbs/01_auth.ipynb 5
|
|
16
|
+
_in_prod = os.getenv('PLASH_PRODUCTION', '') == '1'
|
|
17
|
+
|
|
18
|
+
# %% ../nbs/01_auth.ipynb 6
|
|
19
|
+
def _signin_url(email_re: str=None, hd_re: str=None):
|
|
20
|
+
res = httpx.post(os.environ['PLASH_AUTH_URL'], json=dict(email_re=email_re, hd_re=hd_re),
|
|
21
|
+
auth=(os.environ['PLASH_APP_ID'], os.environ['PLASH_APP_SECRET']),
|
|
22
|
+
headers={'X-PLASH-AUTH-VERSION': __version__}).raise_for_status().json()
|
|
23
|
+
if "warning" in res: warn(res.pop('warning'))
|
|
24
|
+
return res
|
|
25
|
+
|
|
26
|
+
# %% ../nbs/01_auth.ipynb 8
|
|
27
|
+
signin_completed_rt = "/signin_completed"
|
|
28
|
+
|
|
29
|
+
# %% ../nbs/01_auth.ipynb 10
|
|
30
|
+
def mk_signin_url(session: dict, # Session dictionary
|
|
31
|
+
email_re: str=None, # Regex filter for allowed email addresses
|
|
32
|
+
hd_re: str=None): # Regex filter for allowed Google hosted domains
|
|
33
|
+
"Generate a Google Sign-In URL for Plash authentication."
|
|
34
|
+
if not _in_prod: return f"{signin_completed_rt}?signin_reply=mock-sign-in-reply"
|
|
35
|
+
res = _signin_url(email_re, hd_re)
|
|
36
|
+
session['req_id'] = res['req_id']
|
|
37
|
+
return res['plash_signin_url']
|
|
38
|
+
|
|
39
|
+
# %% ../nbs/01_auth.ipynb 12
|
|
40
|
+
def _parse_jwt(reply: str) -> dict:
|
|
41
|
+
"Parse JWT reply and return decoded claims or error info"
|
|
42
|
+
try: decoded = jwt.decode(reply, key=open(Path(__file__).parent / "assets" / "es256_public_key.pem","rb").read(), algorithms=["ES256"],
|
|
43
|
+
options=dict(verify_aud=False, verify_iss=False))
|
|
44
|
+
except Exception as e: return dict(req_id=None, sub=None, err=f'JWT validation failed: {e}')
|
|
45
|
+
return dict(req_id=decoded.get('req_id'), sub=decoded.get('sub'), err=decoded.get('err'))
|
|
46
|
+
|
|
47
|
+
# %% ../nbs/01_auth.ipynb 14
|
|
48
|
+
class PlashAuthError(Exception):
|
|
49
|
+
"""Raised when Plash authentication fails"""
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
# %% ../nbs/01_auth.ipynb 16
|
|
53
|
+
def goog_id_from_signin_reply(session: dict, # Session dictionary containing 'req_id'
|
|
54
|
+
reply: str): # The JWT reply string from Plash after Google authentication
|
|
55
|
+
"Validate Google sign-in reply and returns Google user ID if valid."
|
|
56
|
+
if not _in_prod: return '424242424242424242424'
|
|
57
|
+
parsed = _parse_jwt(reply)
|
|
58
|
+
if session.get('req_id') != parsed['req_id']: raise PlashAuthError("Request originated from a different browser than the one receiving the reply")
|
|
59
|
+
if parsed['err']: raise PlashAuthError(f"Authentication failed: {parsed['err']}")
|
|
60
|
+
return parsed['sub']
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"""The Plash CLI tool"""
|
|
2
2
|
|
|
3
|
-
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_cli.ipynb.
|
|
4
4
|
|
|
5
5
|
# %% auto 0
|
|
6
|
-
__all__ = ['PLASH_CONFIG_HOME', 'PLASH_DOMAIN', 'pat', '
|
|
7
|
-
'
|
|
8
|
-
'deploy', 'view', 'delete', 'endpoint_func', 'logs', 'download', 'apps']
|
|
6
|
+
__all__ = ['PLASH_CONFIG_HOME', 'PLASH_DOMAIN', 'pat', 'log_modes', 'PlashError', 'login', 'create_tar_archive', 'deploy', 'view',
|
|
7
|
+
'delete', 'start', 'stop', 'logs', 'download', 'apps']
|
|
9
8
|
|
|
10
|
-
# %% ../nbs/
|
|
9
|
+
# %% ../nbs/00_cli.ipynb 3
|
|
11
10
|
from fastcore.all import *
|
|
12
11
|
from fastcore.xdg import *
|
|
13
12
|
import secrets, webbrowser, json, httpx, io, tarfile, random, string
|
|
@@ -18,12 +17,12 @@ import io, os, re, tarfile, tomllib
|
|
|
18
17
|
|
|
19
18
|
from . import __version__
|
|
20
19
|
|
|
21
|
-
# %% ../nbs/
|
|
20
|
+
# %% ../nbs/00_cli.ipynb 6
|
|
22
21
|
PLASH_CONFIG_HOME = xdg_config_home() / 'plash_config.json'
|
|
23
22
|
PLASH_DOMAIN = os.getenv("PLASH_DOMAIN","pla.sh") # pla.sh plash-dev.answer.ai localhost:5002
|
|
24
23
|
|
|
25
|
-
# %% ../nbs/
|
|
26
|
-
def
|
|
24
|
+
# %% ../nbs/00_cli.ipynb 7
|
|
25
|
+
def _get_client(cookie_file):
|
|
27
26
|
client = httpx.Client()
|
|
28
27
|
if not cookie_file.exists():
|
|
29
28
|
raise FileNotFoundError("Plash config not found. Please run plash_login and try again.")
|
|
@@ -32,11 +31,11 @@ def get_client(cookie_file):
|
|
|
32
31
|
client.headers.update({'X-PLASH': 'true', 'User-Agent': f'plash_cli/{__version__}'})
|
|
33
32
|
return client
|
|
34
33
|
|
|
35
|
-
# %% ../nbs/
|
|
36
|
-
def
|
|
34
|
+
# %% ../nbs/00_cli.ipynb 8
|
|
35
|
+
def _mk_auth_req(url:str, method:str='get', **kwargs): return getattr(_get_client(PLASH_CONFIG_HOME), method)(url, **kwargs)
|
|
37
36
|
|
|
38
|
-
# %% ../nbs/
|
|
39
|
-
def
|
|
37
|
+
# %% ../nbs/00_cli.ipynb 9
|
|
38
|
+
def _get_app_name(path:Path):
|
|
40
39
|
plash_app = Path(path) / '.plash'
|
|
41
40
|
if not plash_app.exists(): raise FileNotFoundError(f"File not found: {plash_app=}")
|
|
42
41
|
env = parse_env(fn=plash_app)
|
|
@@ -47,13 +46,13 @@ def get_app_name(path:Path):
|
|
|
47
46
|
raise RuntimeError(f"{plash_app=} did not have a PLASH_APP_NAME")
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
# %% ../nbs/
|
|
51
|
-
def
|
|
49
|
+
# %% ../nbs/00_cli.ipynb 10
|
|
50
|
+
def _endpoint(sub='', rt=''):
|
|
52
51
|
p = "http" if "localhost" in PLASH_DOMAIN else "https"
|
|
53
52
|
return f"{p}://{sub}{'.' if sub else ''}{PLASH_DOMAIN}{rt}"
|
|
54
53
|
|
|
55
|
-
# %% ../nbs/
|
|
56
|
-
def
|
|
54
|
+
# %% ../nbs/00_cli.ipynb 11
|
|
55
|
+
def _is_included(path):
|
|
57
56
|
"Returns True if path should be included in deployment"
|
|
58
57
|
if path.name.startswith('.'): return False
|
|
59
58
|
if path.suffix == '.pyc': return False
|
|
@@ -62,35 +61,37 @@ def is_included(path):
|
|
|
62
61
|
'.vscode', '.idea', '.sesskey'}
|
|
63
62
|
return not any(p in excludes for p in path.parts)
|
|
64
63
|
|
|
65
|
-
# %% ../nbs/
|
|
64
|
+
# %% ../nbs/00_cli.ipynb 12
|
|
66
65
|
class PlashError(Exception): pass
|
|
67
66
|
|
|
68
|
-
# %% ../nbs/
|
|
69
|
-
def
|
|
67
|
+
# %% ../nbs/00_cli.ipynb 13
|
|
68
|
+
def _poll_cookies(paircode, interval=1, timeout=180):
|
|
70
69
|
"Poll server for token until received or timeout"
|
|
71
70
|
start = time()
|
|
72
71
|
client = httpx.Client()
|
|
73
|
-
url =
|
|
72
|
+
url = _endpoint(rt=f"/cli_token?paircode={paircode}")
|
|
74
73
|
while time()-start < timeout:
|
|
75
74
|
resp = client.get(url).raise_for_status()
|
|
76
75
|
if resp.text.strip(): return dict(client.cookies)
|
|
77
76
|
sleep(interval)
|
|
78
|
-
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# %% ../nbs/00_cli.ipynb 14
|
|
79
80
|
@call_parse
|
|
80
81
|
def login():
|
|
81
82
|
"Authenticate CLI with server and save config"
|
|
82
83
|
paircode = secrets.token_urlsafe(16)
|
|
83
|
-
login_url = httpx.get(
|
|
84
|
+
login_url = httpx.get(_endpoint(rt=f"/cli_login?paircode={paircode}")).text
|
|
84
85
|
print(f"Opening browser for authentication:\n{login_url}\n")
|
|
85
86
|
webbrowser.open(login_url)
|
|
86
87
|
|
|
87
|
-
cookies =
|
|
88
|
+
cookies = _poll_cookies(paircode)
|
|
88
89
|
if cookies:
|
|
89
90
|
Path(PLASH_CONFIG_HOME).write_text(json.dumps(cookies))
|
|
90
91
|
print(f"Authentication successful! Config saved to {PLASH_CONFIG_HOME}")
|
|
91
92
|
else: print("Authentication timed out.")
|
|
92
93
|
|
|
93
|
-
# %% ../nbs/
|
|
94
|
+
# %% ../nbs/00_cli.ipynb 17
|
|
94
95
|
pat = r'(?m)^# /// (?P<type>[a-zA-Z0-9-]+)$\s(?P<content>(^#(| .*)$\s)+)^# ///$'
|
|
95
96
|
|
|
96
97
|
def _deps(script: bytes | str) -> dict | None:
|
|
@@ -105,8 +106,8 @@ def _deps(script: bytes | str) -> dict | None:
|
|
|
105
106
|
return '\n'.join(tomllib.loads(content)['dependencies'])
|
|
106
107
|
else: return None
|
|
107
108
|
|
|
108
|
-
# %% ../nbs/
|
|
109
|
-
def
|
|
109
|
+
# %% ../nbs/00_cli.ipynb 19
|
|
110
|
+
def _validate_app(path):
|
|
110
111
|
"Validates directory `path` is a deployable Plash app"
|
|
111
112
|
if not (path / 'main.py').exists():
|
|
112
113
|
raise PlashError('A Plash app requires a main.py file.')
|
|
@@ -114,11 +115,11 @@ def validate_app(path):
|
|
|
114
115
|
if deps and (path/"requirements.txt").exists():
|
|
115
116
|
raise PlashError('A Plash app should not contain both a requirements.txt file and inline dependencies (see PEP723).')
|
|
116
117
|
|
|
117
|
-
# %% ../nbs/
|
|
118
|
+
# %% ../nbs/00_cli.ipynb 22
|
|
118
119
|
def create_tar_archive(path:Path, force_data:bool=False) -> tuple[io.BytesIO, int]:
|
|
119
120
|
"Creates a tar archive of a directory, excluding files based on is_included"
|
|
120
121
|
tarz = io.BytesIO()
|
|
121
|
-
files = L(path if path.is_file() else Path(path).iterdir()).filter(
|
|
122
|
+
files = L(path if path.is_file() else Path(path).iterdir()).filter(_is_included)
|
|
122
123
|
if not force_data: files = files.filter(lambda f: f.name != 'data')
|
|
123
124
|
with tarfile.open(fileobj=tarz, mode='w:gz') as tar:
|
|
124
125
|
for f in files: tar.add(f, arcname=f.name)
|
|
@@ -129,7 +130,7 @@ def create_tar_archive(path:Path, force_data:bool=False) -> tuple[io.BytesIO, in
|
|
|
129
130
|
tarz.seek(0)
|
|
130
131
|
return tarz, len(files)
|
|
131
132
|
|
|
132
|
-
# %% ../nbs/
|
|
133
|
+
# %% ../nbs/00_cli.ipynb 23
|
|
133
134
|
def _gen_app_name():
|
|
134
135
|
adjectives = ['admiring', 'adoring', 'amazing', 'awesome', 'beautiful', 'blissful', 'bold', 'brave', 'busy', 'charming', 'clever', 'compassionate', 'confident', 'cool', 'dazzling', 'determined', 'dreamy', 'eager', 'ecstatic', 'elastic', 'elated', 'elegant', 'epic', 'exciting', 'fervent', 'festive', 'flamboyant', 'focused', 'friendly', 'frosty', 'funny', 'gallant', 'gifted', 'goofy', 'gracious', 'great', 'happy', 'hopeful', 'hungry', 'inspiring', 'intelligent', 'interesting', 'jolly', 'jovial', 'keen', 'kind', 'laughing', 'loving', 'lucid', 'magical', 'modest', 'nice', 'nifty', 'nostalgic', 'objective', 'optimistic', 'peaceful', 'pensive', 'practical', 'priceless', 'quirky', 'quizzical', 'relaxed', 'reverent', 'romantic', 'serene', 'sharp', 'silly', 'sleepy', 'stoic', 'sweet', 'tender', 'trusting', 'upbeat', 'vibrant', 'vigilant', 'vigorous', 'wizardly', 'wonderful', 'youthful', 'zealous', 'zen', 'golden', 'silver', 'crimson', 'azure', 'emerald', 'violet', 'amber', 'coral', 'turquoise', 'lavender', 'minty', 'citrus', 'vanilla', 'woody', 'floral', 'fresh', 'gentle', 'sparkling', 'precise', 'curious']
|
|
135
136
|
nouns = ['tiger', 'eagle', 'river', 'mountain', 'forest', 'ocean', 'star', 'moon', 'wind', 'dragon', 'phoenix', 'wolf', 'bear', 'lion', 'shark', 'falcon', 'raven', 'crystal', 'diamond', 'ruby', 'sapphire', 'pearl', 'wave', 'tide', 'cloud', 'rainbow', 'sunset', 'sunrise', 'galaxy', 'comet', 'meteor', 'planet', 'nebula', 'cosmos', 'universe', 'atom', 'photon', 'quantum', 'matrix', 'cipher', 'code', 'signal', 'pulse', 'beam', 'ray', 'spark', 'frost', 'ice', 'snow', 'mist', 'fog', 'dew', 'rain', 'hail', 'helix', 'prism', 'lens', 'mirror', 'echo', 'heart', 'mind', 'dream', 'vision', 'hope', 'wish', 'magic', 'spell', 'charm', 'rune', 'symbol', 'token', 'key', 'door', 'gate', 'bridge', 'tower', 'castle', 'fortress', 'shield', 'dolphin', 'whale', 'penguin', 'butterfly', 'hummingbird', 'deer', 'rabbit', 'fox', 'otter', 'panda', 'koala', 'zebra', 'giraffe', 'elephant', 'valley', 'canyon', 'meadow', 'prairie', 'island', 'lake', 'pond', 'stream', 'waterfall', 'cliff', 'peak', 'hill', 'grove', 'garden', 'sunlight', 'breeze', 'melody', 'sparkle', 'whirlpool', 'windmill', 'carousel', 'spiral', 'glow']
|
|
@@ -137,7 +138,7 @@ def _gen_app_name():
|
|
|
137
138
|
suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=3))
|
|
138
139
|
return f"{random.choice(adjectives)}-{random.choice(nouns)}-{random.choice(verbs)}-{suffix}"
|
|
139
140
|
|
|
140
|
-
# %% ../nbs/
|
|
141
|
+
# %% ../nbs/00_cli.ipynb 24
|
|
141
142
|
@call_parse
|
|
142
143
|
def deploy(
|
|
143
144
|
path:Path=Path('.'), # Path to project
|
|
@@ -145,51 +146,51 @@ def deploy(
|
|
|
145
146
|
force_data:bool=False): # Overwrite data/ directory during deployment
|
|
146
147
|
"""
|
|
147
148
|
Deploys app to production. By default, this command erases all files in your app which are not in data/.
|
|
148
|
-
Then uploads all files and folders, except paths starting with
|
|
149
|
-
If `--
|
|
150
|
-
including `data/`, except paths starting with
|
|
149
|
+
Then uploads all files and folders, except paths starting with `.` and except the local data/ directory.
|
|
150
|
+
If `--force_data` is used, then it erases all files in production. Then it uploads all files and folders,
|
|
151
|
+
including `data/`, except paths starting with `.`.
|
|
151
152
|
"""
|
|
152
153
|
print('Initializing deployment...')
|
|
153
154
|
if name == '': print('Error: App name cannot be an empty string'); return
|
|
154
155
|
if not path.is_dir(): print("Error: Path should point to the project directory"); return
|
|
155
|
-
try:
|
|
156
|
+
try: _validate_app(path)
|
|
156
157
|
except PlashError as e: print(f"Error: {str(e)}\nInvalid path: {path}"); return
|
|
157
158
|
|
|
158
159
|
try:
|
|
159
|
-
if not name: name =
|
|
160
|
+
if not name: name = _get_app_name(path)
|
|
160
161
|
except FileNotFoundError:
|
|
161
162
|
plash_app = path / '.plash'
|
|
162
163
|
name = _gen_app_name()
|
|
163
164
|
plash_app.write_text(f'export PLASH_APP_NAME={name}')
|
|
164
165
|
|
|
165
166
|
tarz, _ = create_tar_archive(path, force_data)
|
|
166
|
-
resp =
|
|
167
|
+
resp = _mk_auth_req(_endpoint(rt="/upload"), "post", files={'file': tarz}, timeout=300.0,
|
|
167
168
|
data={'name': name, 'force_data': force_data})
|
|
168
169
|
if resp.status_code == 200:
|
|
169
170
|
print('✅ Upload complete! Your app is currently being built.')
|
|
170
|
-
print(f'It will be live at {name if "." in name else
|
|
171
|
+
print(f'It will be live at {name if "." in name else _endpoint(sub=name)}')
|
|
171
172
|
else: print(f'Failure: {resp.status_code}\n{resp.text}')
|
|
172
173
|
|
|
173
|
-
# %% ../nbs/
|
|
174
|
+
# %% ../nbs/00_cli.ipynb 27
|
|
174
175
|
@call_parse
|
|
175
176
|
def view(
|
|
176
177
|
path:Path=Path('.'), # Path to project directory
|
|
177
178
|
name:str=None, # Overrides the .plash file in project root if provided
|
|
178
179
|
):
|
|
179
180
|
"Open your app in the browser"
|
|
180
|
-
if not name: name =
|
|
181
|
-
url = name if '.' in name else
|
|
181
|
+
if not name: name = _get_app_name(path)
|
|
182
|
+
url = name if '.' in name else _endpoint(sub=name)
|
|
182
183
|
print(f"Opening browser to view app :\n{url}\n")
|
|
183
184
|
webbrowser.open(url)
|
|
184
185
|
|
|
185
|
-
# %% ../nbs/
|
|
186
|
+
# %% ../nbs/00_cli.ipynb 30
|
|
186
187
|
@call_parse
|
|
187
188
|
def delete(
|
|
188
189
|
path:Path=Path('.'), # Path to project
|
|
189
190
|
name:str=None, # Overrides the .plash file in project root if provided
|
|
190
191
|
force:bool=False): # Skip confirmation prompt
|
|
191
192
|
'Delete your deployed app'
|
|
192
|
-
if not name: name =
|
|
193
|
+
if not name: name = _get_app_name(path)
|
|
193
194
|
if not force:
|
|
194
195
|
confirm = input(f"Are you sure you want to delete app '{name}'? This action cannot be undone. [y/N]: ")
|
|
195
196
|
if confirm.lower() not in ['y', 'yes']:
|
|
@@ -197,34 +198,29 @@ def delete(
|
|
|
197
198
|
return
|
|
198
199
|
|
|
199
200
|
print(f"Deleting app '{name}'...")
|
|
200
|
-
r =
|
|
201
|
+
r = _mk_auth_req(_endpoint(rt=f"/delete?name={name}"), "delete")
|
|
201
202
|
return r.text
|
|
202
203
|
|
|
203
|
-
# %% ../nbs/
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if not name: name = get_app_name(path)
|
|
211
|
-
r = mk_auth_req(endpoint(rt=f"{endpoint_name}?name={name}"))
|
|
212
|
-
return r.text
|
|
213
|
-
|
|
214
|
-
# Set the function name and docstring
|
|
215
|
-
func.__name__ = endpoint_name
|
|
216
|
-
func.__doc__ = f"Access the '{endpoint_name}' endpoint for your app"
|
|
217
|
-
|
|
218
|
-
return call_parse(func)
|
|
204
|
+
# %% ../nbs/00_cli.ipynb 33
|
|
205
|
+
@call_parse
|
|
206
|
+
def start(path:Path=Path('.'), name:str=None):
|
|
207
|
+
"Start your deployed app"
|
|
208
|
+
if not name: name = _get_app_name(path)
|
|
209
|
+
r = _mk_auth_req(_endpoint(rt=f"/start?name={name}"))
|
|
210
|
+
return r.text
|
|
219
211
|
|
|
220
|
-
#
|
|
221
|
-
|
|
222
|
-
|
|
212
|
+
# %% ../nbs/00_cli.ipynb 36
|
|
213
|
+
@call_parse
|
|
214
|
+
def stop(path:Path=Path('.'), name:str=None):
|
|
215
|
+
"Stop your deployed app"
|
|
216
|
+
if not name: name = _get_app_name(path)
|
|
217
|
+
r = _mk_auth_req(_endpoint(rt=f"/stop?name={name}"))
|
|
218
|
+
return r.text
|
|
223
219
|
|
|
224
|
-
# %% ../nbs/
|
|
220
|
+
# %% ../nbs/00_cli.ipynb 39
|
|
225
221
|
log_modes = str_enum('log_modes', 'build', 'app')
|
|
226
222
|
|
|
227
|
-
# %% ../nbs/
|
|
223
|
+
# %% ../nbs/00_cli.ipynb 40
|
|
228
224
|
@call_parse
|
|
229
225
|
def logs(
|
|
230
226
|
path:Path=Path('.'), # Path to project
|
|
@@ -232,12 +228,12 @@ def logs(
|
|
|
232
228
|
mode:log_modes='build', # Choose between build or app logs
|
|
233
229
|
tail:bool=False): # Tail the logs
|
|
234
230
|
'Prints the logs for your deployed app'
|
|
235
|
-
if not name: name =
|
|
231
|
+
if not name: name = _get_app_name(path)
|
|
236
232
|
if tail:
|
|
237
233
|
text = ''
|
|
238
234
|
while True:
|
|
239
235
|
try:
|
|
240
|
-
r =
|
|
236
|
+
r = _mk_auth_req(_endpoint(rt=f"/logs?name={name}&mode={mode}"))
|
|
241
237
|
if r.status_code == 200:
|
|
242
238
|
print(r.text[len(text):], end='') # Only print updates
|
|
243
239
|
text = r.text
|
|
@@ -247,30 +243,30 @@ def logs(
|
|
|
247
243
|
print(f"Error: {r.status_code}")
|
|
248
244
|
except KeyboardInterrupt:
|
|
249
245
|
return "\nExiting"
|
|
250
|
-
r =
|
|
246
|
+
r = _mk_auth_req(endpoint(rt=f"/logs?name={name}&mode={mode}"))
|
|
251
247
|
return r.text
|
|
252
248
|
|
|
253
|
-
# %% ../nbs/
|
|
249
|
+
# %% ../nbs/00_cli.ipynb 43
|
|
254
250
|
@call_parse
|
|
255
251
|
def download(
|
|
256
252
|
path:Path=Path('.'), # Path to project
|
|
257
253
|
name:str=None, # Overrides the .plash file in project root if provided
|
|
258
254
|
save_path:Path=Path("./download/")): # Save path (optional)
|
|
259
255
|
'Download your deployed app'
|
|
260
|
-
if not name: name =
|
|
256
|
+
if not name: name = _get_app_name(path)
|
|
261
257
|
try: save_path.mkdir(exist_ok=False)
|
|
262
258
|
except: print(f"ERROR: Save path ({save_path}) already exists. Please rename or delete this folder to avoid accidental overwrites.")
|
|
263
259
|
else:
|
|
264
|
-
response = mk_auth_req(
|
|
260
|
+
response = mk_auth_req(_endpoint(rt=f'/download?name={name}')).raise_for_status()
|
|
265
261
|
file_bytes = io.BytesIO(response.content)
|
|
266
262
|
with tarfile.open(fileobj=file_bytes, mode="r:gz") as tar: tar.extractall(path=save_path)
|
|
267
263
|
print(f"Downloaded your app to: {save_path}")
|
|
268
264
|
|
|
269
|
-
# %% ../nbs/
|
|
265
|
+
# %% ../nbs/00_cli.ipynb 46
|
|
270
266
|
@call_parse
|
|
271
267
|
def apps(verbose:bool=False):
|
|
272
268
|
"List your deployed apps (verbose shows status table: 1=running, 0=stopped)"
|
|
273
|
-
r =
|
|
269
|
+
r = _mk_auth_req(_endpoint(rt="/user_apps")).raise_for_status()
|
|
274
270
|
apps = r.json()
|
|
275
271
|
if not apps: return "You don't have any deployed Plash apps."
|
|
276
272
|
if verbose: [print(f"{a['running']} {a['name']}") for a in apps]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plash_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: CLI for the Plash hosting service
|
|
5
5
|
Home-page: https://github.com/AnswerDotAI/plash_cli
|
|
6
6
|
Author: Jeremy Howard
|
|
@@ -18,6 +18,9 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Requires-Dist: fastcore
|
|
20
20
|
Requires-Dist: httpx>=0.28.1
|
|
21
|
+
Requires-Dist: python-dotenv
|
|
22
|
+
Requires-Dist: pyjwt
|
|
23
|
+
Requires-Dist: cryptography
|
|
21
24
|
Provides-Extra: dev
|
|
22
25
|
Requires-Dist: bash_kernel; extra == "dev"
|
|
23
26
|
Requires-Dist: nbdev; extra == "dev"
|
|
@@ -185,13 +188,18 @@ When you visit that page you should see “Hello, World!”
|
|
|
185
188
|
Learn more about what Plash has to offer in the rest of the docs at:
|
|
186
189
|
<https://docs.pla.sh>
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
looking at the official docs at: <https://fastht.ml/docs/>.
|
|
190
|
-
|
|
191
|
-
Particularly, we recommend the following:
|
|
191
|
+
To learn more about creating web apps with FastHTML, we recommend
|
|
192
|
+
looking at the official docs at: <https://fastht.ml/docs/>. In
|
|
193
|
+
particular, we recommend the following:
|
|
192
194
|
|
|
193
195
|
1. [OAuth](https://fastht.ml/docs/explains/oauth.html) - Setup
|
|
194
196
|
authentication for your Plash App with Google Sign-In or other OAuth
|
|
195
197
|
Providers.
|
|
196
198
|
2. [Stripe](https://fastht.ml/docs/explains/stripe.html) - Accept
|
|
197
199
|
payments for your products hosted on Plash with Stripe.
|
|
200
|
+
|
|
201
|
+
Would you like to use a different python web framework? Then check out
|
|
202
|
+
the [examples
|
|
203
|
+
section](https://github.com/AnswerDotAI/plash_cli/tree/main/examples) of
|
|
204
|
+
the repo, it has examples for many popular python web frameworks like
|
|
205
|
+
FastAPI, Gradio, and Streamlit.
|
|
@@ -7,11 +7,13 @@ setup.py
|
|
|
7
7
|
plash_cli/__init__.py
|
|
8
8
|
plash_cli/_bash_magic.py
|
|
9
9
|
plash_cli/_modidx.py
|
|
10
|
-
plash_cli/
|
|
10
|
+
plash_cli/auth.py
|
|
11
|
+
plash_cli/cli.py
|
|
11
12
|
plash_cli.egg-info/PKG-INFO
|
|
12
13
|
plash_cli.egg-info/SOURCES.txt
|
|
13
14
|
plash_cli.egg-info/dependency_links.txt
|
|
14
15
|
plash_cli.egg-info/entry_points.txt
|
|
15
16
|
plash_cli.egg-info/not-zip-safe
|
|
16
17
|
plash_cli.egg-info/requires.txt
|
|
17
|
-
plash_cli.egg-info/top_level.txt
|
|
18
|
+
plash_cli.egg-info/top_level.txt
|
|
19
|
+
plash_cli/assets/es256_public_key.pem
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
[console_scripts]
|
|
2
|
+
plash_apps = plash_cli.cli:apps
|
|
3
|
+
plash_delete = plash_cli.cli:delete
|
|
4
|
+
plash_deploy = plash_cli.cli:deploy
|
|
5
|
+
plash_download = plash_cli.cli:download
|
|
6
|
+
plash_login = plash_cli.cli:login
|
|
7
|
+
plash_logs = plash_cli.cli:logs
|
|
8
|
+
plash_start = plash_cli.cli:start
|
|
9
|
+
plash_stop = plash_cli.cli:stop
|
|
10
|
+
plash_view = plash_cli.cli:view
|
|
11
|
+
|
|
12
|
+
[nbdev]
|
|
13
|
+
plash_cli = plash_cli._modidx:d
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
jupyter_hooks = False
|
|
3
3
|
repo = plash_cli
|
|
4
4
|
lib_name = plash_cli
|
|
5
|
-
version = 0.
|
|
5
|
+
version = 0.3.1
|
|
6
6
|
min_python = 3.11
|
|
7
7
|
license = apache2
|
|
8
8
|
black_formatting = False
|
|
@@ -27,17 +27,17 @@ keywords = nbdev jupyter notebook python
|
|
|
27
27
|
language = English
|
|
28
28
|
status = 3
|
|
29
29
|
user = AnswerDotAI
|
|
30
|
-
requirements = fastcore httpx>=0.28.1
|
|
30
|
+
requirements = fastcore httpx>=0.28.1 python-dotenv pyjwt cryptography
|
|
31
31
|
dev_requirements = bash_kernel nbdev
|
|
32
|
-
console_scripts = plash_deploy=plash_cli.
|
|
33
|
-
plash_login=plash_cli.
|
|
34
|
-
plash_view=plash_cli.
|
|
35
|
-
plash_logs=plash_cli.
|
|
36
|
-
plash_delete=plash_cli.
|
|
37
|
-
plash_stop=plash_cli.
|
|
38
|
-
plash_start=plash_cli.
|
|
39
|
-
plash_download=plash_cli.
|
|
40
|
-
plash_apps=plash_cli.
|
|
32
|
+
console_scripts = plash_deploy=plash_cli.cli:deploy
|
|
33
|
+
plash_login=plash_cli.cli:login
|
|
34
|
+
plash_view=plash_cli.cli:view
|
|
35
|
+
plash_logs=plash_cli.cli:logs
|
|
36
|
+
plash_delete=plash_cli.cli:delete
|
|
37
|
+
plash_stop=plash_cli.cli:stop
|
|
38
|
+
plash_start=plash_cli.cli:start
|
|
39
|
+
plash_download=plash_cli.cli:download
|
|
40
|
+
plash_apps=plash_cli.cli:apps
|
|
41
41
|
readme_nb = index.ipynb
|
|
42
42
|
allowed_metadata_keys =
|
|
43
43
|
allowed_cell_metadata_keys =
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.2"
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Autogenerated by nbdev
|
|
2
|
-
|
|
3
|
-
d = { 'settings': { 'branch': 'main',
|
|
4
|
-
'doc_baseurl': '/plash_cli',
|
|
5
|
-
'doc_host': 'https://AnswerDotAI.github.io',
|
|
6
|
-
'git_url': 'https://github.com/AnswerDotAI/plash_cli',
|
|
7
|
-
'lib_path': 'plash_cli'},
|
|
8
|
-
'syms': { 'plash_cli.core': { 'plash_cli.core.PlashError': ('core.html#plasherror', 'plash_cli/core.py'),
|
|
9
|
-
'plash_cli.core._deps': ('core.html#_deps', 'plash_cli/core.py'),
|
|
10
|
-
'plash_cli.core._gen_app_name': ('core.html#_gen_app_name', 'plash_cli/core.py'),
|
|
11
|
-
'plash_cli.core.apps': ('core.html#apps', 'plash_cli/core.py'),
|
|
12
|
-
'plash_cli.core.create_tar_archive': ('core.html#create_tar_archive', 'plash_cli/core.py'),
|
|
13
|
-
'plash_cli.core.delete': ('core.html#delete', 'plash_cli/core.py'),
|
|
14
|
-
'plash_cli.core.deploy': ('core.html#deploy', 'plash_cli/core.py'),
|
|
15
|
-
'plash_cli.core.download': ('core.html#download', 'plash_cli/core.py'),
|
|
16
|
-
'plash_cli.core.endpoint': ('core.html#endpoint', 'plash_cli/core.py'),
|
|
17
|
-
'plash_cli.core.endpoint_func': ('core.html#endpoint_func', 'plash_cli/core.py'),
|
|
18
|
-
'plash_cli.core.get_app_name': ('core.html#get_app_name', 'plash_cli/core.py'),
|
|
19
|
-
'plash_cli.core.get_client': ('core.html#get_client', 'plash_cli/core.py'),
|
|
20
|
-
'plash_cli.core.is_included': ('core.html#is_included', 'plash_cli/core.py'),
|
|
21
|
-
'plash_cli.core.login': ('core.html#login', 'plash_cli/core.py'),
|
|
22
|
-
'plash_cli.core.logs': ('core.html#logs', 'plash_cli/core.py'),
|
|
23
|
-
'plash_cli.core.mk_auth_req': ('core.html#mk_auth_req', 'plash_cli/core.py'),
|
|
24
|
-
'plash_cli.core.poll_cookies': ('core.html#poll_cookies', 'plash_cli/core.py'),
|
|
25
|
-
'plash_cli.core.validate_app': ('core.html#validate_app', 'plash_cli/core.py'),
|
|
26
|
-
'plash_cli.core.view': ('core.html#view', 'plash_cli/core.py')}}}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
[console_scripts]
|
|
2
|
-
plash_apps = plash_cli.core:apps
|
|
3
|
-
plash_delete = plash_cli.core:delete
|
|
4
|
-
plash_deploy = plash_cli.core:deploy
|
|
5
|
-
plash_download = plash_cli.core:download
|
|
6
|
-
plash_login = plash_cli.core:login
|
|
7
|
-
plash_logs = plash_cli.core:logs
|
|
8
|
-
plash_start = plash_cli.core:start
|
|
9
|
-
plash_stop = plash_cli.core:stop
|
|
10
|
-
plash_view = plash_cli.core:view
|
|
11
|
-
|
|
12
|
-
[nbdev]
|
|
13
|
-
plash_cli = plash_cli._modidx:d
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|