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.
@@ -3,3 +3,4 @@ include LICENSE
3
3
  include CONTRIBUTING.md
4
4
  include README.md
5
5
  recursive-exclude * __pycache__
6
+ include plash_cli/assets/es256_public_key.pem
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plash_cli
3
- Version: 0.2.2
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
- For learning more about creating web apps with FastHTML, we recommend
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
- For learning more about creating web apps with FastHTML, we recommend
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,4 @@
1
+ -----BEGIN PUBLIC KEY-----
2
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEmAlaJd3pPsLNDxMf+gG1e+0DfSnS
3
+ mfrJiP5rgj8GL/xwhALJl9DOrw0gBh1H3Q2/XQvs+Df0rWXJ5bryn2ZPmg==
4
+ -----END PUBLIC KEY-----
@@ -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/00_core.ipynb.
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', 'stop', 'start', 'log_modes', 'get_client', 'mk_auth_req', 'get_app_name',
7
- 'endpoint', 'is_included', 'PlashError', 'poll_cookies', 'login', 'validate_app', 'create_tar_archive',
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/00_core.ipynb 2
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/00_core.ipynb 5
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/00_core.ipynb 6
26
- def get_client(cookie_file):
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/00_core.ipynb 7
36
- def mk_auth_req(url:str, method:str='get', **kwargs): return getattr(get_client(PLASH_CONFIG_HOME), method)(url, **kwargs)
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/00_core.ipynb 8
39
- def get_app_name(path:Path):
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/00_core.ipynb 9
51
- def endpoint(sub='', rt=''):
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/00_core.ipynb 10
56
- def is_included(path):
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/00_core.ipynb 11
64
+ # %% ../nbs/00_cli.ipynb 12
66
65
  class PlashError(Exception): pass
67
66
 
68
- # %% ../nbs/00_core.ipynb 13
69
- def poll_cookies(paircode, interval=1, timeout=180):
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 = endpoint(rt=f"/cli_token?paircode={paircode}")
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(endpoint(rt=f"/cli_login?paircode={paircode}")).text
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 = poll_cookies(paircode)
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/00_core.ipynb 16
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/00_core.ipynb 19
109
- def validate_app(path):
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/00_core.ipynb 24
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(is_included)
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/00_core.ipynb 25
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/00_core.ipynb 26
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 '.' and except the local data/ directory.
149
- If `--force data` is used, then it erases all files in production. Then it uploads all files and folders,
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: validate_app(path)
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 = get_app_name(path)
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 = mk_auth_req(endpoint(rt="/upload"), "post", files={'file': tarz}, timeout=300.0,
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 endpoint(sub=name)}')
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/00_core.ipynb 28
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 = get_app_name(path)
181
- url = name if '.' in name else endpoint(sub=name)
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/00_core.ipynb 30
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 = get_app_name(path)
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 = mk_auth_req(endpoint(rt=f"/delete?name={name}"), "delete")
201
+ r = _mk_auth_req(_endpoint(rt=f"/delete?name={name}"), "delete")
201
202
  return r.text
202
203
 
203
- # %% ../nbs/00_core.ipynb 32
204
- def endpoint_func(endpoint_name):
205
- 'Creates a function for a specific API endpoint'
206
- def func(
207
- path:Path=Path('.'), # Path to project
208
- name:str=None, # Overrides the .plash file in project root if provided
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
- # Create endpoint-specific functions
221
- stop = endpoint_func('/stop')
222
- start = endpoint_func('/start')
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/00_core.ipynb 34
220
+ # %% ../nbs/00_cli.ipynb 39
225
221
  log_modes = str_enum('log_modes', 'build', 'app')
226
222
 
227
- # %% ../nbs/00_core.ipynb 35
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 = get_app_name(path)
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 = mk_auth_req(endpoint(rt=f"/logs?name={name}&mode={mode}"))
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 = mk_auth_req(endpoint(rt=f"/logs?name={name}&mode={mode}"))
246
+ r = _mk_auth_req(endpoint(rt=f"/logs?name={name}&mode={mode}"))
251
247
  return r.text
252
248
 
253
- # %% ../nbs/00_core.ipynb 37
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 = get_app_name(path)
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(endpoint(rt=f'/download?name={name}')).raise_for_status()
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/00_core.ipynb 39
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 = mk_auth_req(endpoint(rt="/user_apps")).raise_for_status()
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.2.2
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
- For learning more about creating web apps with FastHTML, we recommend
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/core.py
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
@@ -1,5 +1,8 @@
1
1
  fastcore
2
2
  httpx>=0.28.1
3
+ python-dotenv
4
+ pyjwt
5
+ cryptography
3
6
 
4
7
  [dev]
5
8
  bash_kernel
@@ -2,7 +2,7 @@
2
2
  jupyter_hooks = False
3
3
  repo = plash_cli
4
4
  lib_name = plash_cli
5
- version = 0.2.2
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.core:deploy
33
- plash_login=plash_cli.core:login
34
- plash_view=plash_cli.core:view
35
- plash_logs=plash_cli.core:logs
36
- plash_delete=plash_cli.core:delete
37
- plash_stop=plash_cli.core:stop
38
- plash_start=plash_cli.core:start
39
- plash_download=plash_cli.core:download
40
- plash_apps=plash_cli.core:apps
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