litelambda-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.
- litelambda_cli-0.1.0/PKG-INFO +71 -0
- litelambda_cli-0.1.0/README.md +55 -0
- litelambda_cli-0.1.0/litelambda/__init__.py +6 -0
- litelambda_cli-0.1.0/litelambda/client.py +82 -0
- litelambda_cli-0.1.0/litelambda/config.py +57 -0
- litelambda_cli-0.1.0/litelambda/main.py +307 -0
- litelambda_cli-0.1.0/litelambda_cli.egg-info/PKG-INFO +71 -0
- litelambda_cli-0.1.0/litelambda_cli.egg-info/SOURCES.txt +11 -0
- litelambda_cli-0.1.0/litelambda_cli.egg-info/dependency_links.txt +1 -0
- litelambda_cli-0.1.0/litelambda_cli.egg-info/entry_points.txt +2 -0
- litelambda_cli-0.1.0/litelambda_cli.egg-info/top_level.txt +1 -0
- litelambda_cli-0.1.0/pyproject.toml +32 -0
- litelambda_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litelambda-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CLI for LiteLambda — Serverless Python Cron Job Platform
|
|
5
|
+
Author-email: LiteLambda <support@litelambda.in>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://litelambda.in
|
|
8
|
+
Project-URL: Documentation, https://litelambda.in/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/litelambda/litelambda-cli
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# LiteLambda CLI (`litelambda`)
|
|
18
|
+
|
|
19
|
+
Official command-line interface for [LiteLambda](https://litelambda.in) — serverless Python cron job hosting without managing servers.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install litelambda-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Login
|
|
30
|
+
Generate an API token from your [LiteLambda Settings](https://litelambda.in/settings/#api-tokens), then run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
litelambda login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Check Account
|
|
37
|
+
```bash
|
|
38
|
+
litelambda whoami
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Deploy a Python Script
|
|
42
|
+
Deploy any Python file on a cron schedule:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
litelambda deploy scraper.py --schedule "0 9 * * *" --name "Morning Scraper"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
With pip dependencies:
|
|
49
|
+
```bash
|
|
50
|
+
litelambda deploy bot.py --schedule "*/15 * * * *" --requirements requirements.txt
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. List All Jobs
|
|
54
|
+
```bash
|
|
55
|
+
litelambda list
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 5. Run a Job Manually
|
|
59
|
+
```bash
|
|
60
|
+
litelambda run "Morning Scraper"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. View Execution Logs
|
|
64
|
+
```bash
|
|
65
|
+
litelambda logs "Morning Scraper"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# LiteLambda CLI (`litelambda`)
|
|
2
|
+
|
|
3
|
+
Official command-line interface for [LiteLambda](https://litelambda.in) — serverless Python cron job hosting without managing servers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install litelambda-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
### 1. Login
|
|
14
|
+
Generate an API token from your [LiteLambda Settings](https://litelambda.in/settings/#api-tokens), then run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
litelambda login
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Check Account
|
|
21
|
+
```bash
|
|
22
|
+
litelambda whoami
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 3. Deploy a Python Script
|
|
26
|
+
Deploy any Python file on a cron schedule:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
litelambda deploy scraper.py --schedule "0 9 * * *" --name "Morning Scraper"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
With pip dependencies:
|
|
33
|
+
```bash
|
|
34
|
+
litelambda deploy bot.py --schedule "*/15 * * * *" --requirements requirements.txt
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 4. List All Jobs
|
|
38
|
+
```bash
|
|
39
|
+
litelambda list
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 5. Run a Job Manually
|
|
43
|
+
```bash
|
|
44
|
+
litelambda run "Morning Scraper"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 6. View Execution Logs
|
|
48
|
+
```bash
|
|
49
|
+
litelambda logs "Morning Scraper"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## License
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/litelambda/client.py
|
|
3
|
+
Zero-dependency HTTP client for LiteLambda REST API.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import urllib.request
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
from litelambda.config import get_api_url, get_api_token
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class APIError(Exception):
|
|
14
|
+
def __init__(self, message: str, status_code: Optional[int] = None, error_code: Optional[str] = None):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
self.error_code = error_code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LiteLambdaClient:
|
|
21
|
+
def __init__(self, token: Optional[str] = None, base_url: Optional[str] = None):
|
|
22
|
+
self.token = token or get_api_token()
|
|
23
|
+
self.base_url = (base_url or get_api_url()).rstrip("/")
|
|
24
|
+
|
|
25
|
+
def _request(self, method: str, endpoint: str, data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
26
|
+
url = f"{self.base_url}/{endpoint.lstrip('/')}"
|
|
27
|
+
headers = {
|
|
28
|
+
"User-Agent": "litelambda-cli/0.1.0",
|
|
29
|
+
"Accept": "application/json",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if self.token:
|
|
33
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
34
|
+
|
|
35
|
+
body_bytes = None
|
|
36
|
+
if data is not None:
|
|
37
|
+
headers["Content-Type"] = "application/json"
|
|
38
|
+
body_bytes = json.dumps(data).encode("utf-8")
|
|
39
|
+
|
|
40
|
+
req = urllib.request.Request(url, data=body_bytes, headers=headers, method=method.upper())
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
with urllib.request.urlopen(req, timeout=30) as response:
|
|
44
|
+
resp_data = response.read().decode("utf-8")
|
|
45
|
+
if resp_data:
|
|
46
|
+
return json.loads(resp_data)
|
|
47
|
+
return {}
|
|
48
|
+
except urllib.error.HTTPError as e:
|
|
49
|
+
raw_err = e.read().decode("utf-8")
|
|
50
|
+
err_msg = f"HTTP {e.code}: {e.reason}"
|
|
51
|
+
err_code = None
|
|
52
|
+
try:
|
|
53
|
+
parsed = json.loads(raw_err)
|
|
54
|
+
if isinstance(parsed, dict):
|
|
55
|
+
err_msg = parsed.get("message") or parsed.get("error") or err_msg
|
|
56
|
+
err_code = parsed.get("error_code")
|
|
57
|
+
except Exception:
|
|
58
|
+
if raw_err:
|
|
59
|
+
err_msg = f"{err_msg} - {raw_err[:200]}"
|
|
60
|
+
raise APIError(err_msg, status_code=e.code, error_code=err_code)
|
|
61
|
+
except urllib.error.URLError as e:
|
|
62
|
+
raise APIError(f"Network error connecting to {self.base_url}: {e.reason}")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise APIError(f"Unexpected error: {str(e)}")
|
|
65
|
+
|
|
66
|
+
def verify_auth(self) -> Dict[str, Any]:
|
|
67
|
+
return self._request("GET", "/auth/verify")
|
|
68
|
+
|
|
69
|
+
def list_crons(self) -> Dict[str, Any]:
|
|
70
|
+
return self._request("GET", "/crons")
|
|
71
|
+
|
|
72
|
+
def get_cron(self, id_or_name: str) -> Dict[str, Any]:
|
|
73
|
+
return self._request("GET", f"/crons/{urllib.parse.quote(id_or_name)}")
|
|
74
|
+
|
|
75
|
+
def deploy_cron(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
76
|
+
return self._request("POST", "/crons", data=payload)
|
|
77
|
+
|
|
78
|
+
def run_cron(self, id_or_name: str) -> Dict[str, Any]:
|
|
79
|
+
return self._request("POST", f"/crons/{urllib.parse.quote(id_or_name)}/run")
|
|
80
|
+
|
|
81
|
+
def get_logs(self, id_or_name: str) -> Dict[str, Any]:
|
|
82
|
+
return self._request("GET", f"/crons/{urllib.parse.quote(id_or_name)}/logs")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/litelambda/config.py
|
|
3
|
+
Local configuration and credential management (~/.litelambda/credentials.json).
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
DEFAULT_API_URL = "https://litelambda.in/api/v1"
|
|
10
|
+
CONFIG_DIR = Path.home() / ".litelambda"
|
|
11
|
+
CREDENTIALS_FILE = CONFIG_DIR / "credentials.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_api_url() -> str:
|
|
15
|
+
"""Returns the API base URL, checking env var LITELAMBDA_API_URL first."""
|
|
16
|
+
return os.environ.get("LITELAMBDA_API_URL", DEFAULT_API_URL).rstrip("/")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_api_token() -> str | None:
|
|
20
|
+
"""Returns the API token from env var or credentials file."""
|
|
21
|
+
env_token = os.environ.get("LITELAMBDA_API_TOKEN")
|
|
22
|
+
if env_token:
|
|
23
|
+
return env_token.strip()
|
|
24
|
+
|
|
25
|
+
if not CREDENTIALS_FILE.exists():
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
with open(CREDENTIALS_FILE, "r", encoding="utf-8") as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
return data.get("api_token")
|
|
32
|
+
except Exception:
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def save_credentials(token: str, user_email: str | None = None) -> None:
|
|
37
|
+
"""Saves API token to ~/.litelambda/credentials.json."""
|
|
38
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
payload = {
|
|
40
|
+
"api_token": token.strip(),
|
|
41
|
+
"user_email": user_email,
|
|
42
|
+
"api_url": get_api_url(),
|
|
43
|
+
}
|
|
44
|
+
with open(CREDENTIALS_FILE, "w", encoding="utf-8") as f:
|
|
45
|
+
json.dump(payload, f, indent=2)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
# Secure file permissions (owner rw only)
|
|
49
|
+
os.chmod(CREDENTIALS_FILE, 0o600)
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def clear_credentials() -> None:
|
|
55
|
+
"""Removes stored credentials."""
|
|
56
|
+
if CREDENTIALS_FILE.exists():
|
|
57
|
+
CREDENTIALS_FILE.unlink()
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli/litelambda/main.py
|
|
3
|
+
CLI entrypoint for LiteLambda (litelambda-cli).
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
# Add package parent to sys.path if running directly as script
|
|
11
|
+
pkg_parent = str(Path(__file__).resolve().parent.parent)
|
|
12
|
+
if pkg_parent not in sys.path:
|
|
13
|
+
sys.path.insert(0, pkg_parent)
|
|
14
|
+
|
|
15
|
+
from litelambda.client import LiteLambdaClient, APIError
|
|
16
|
+
from litelambda.config import (
|
|
17
|
+
get_api_token,
|
|
18
|
+
save_credentials,
|
|
19
|
+
clear_credentials,
|
|
20
|
+
get_api_url,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Terminal styling helpers
|
|
24
|
+
BOLD = "\033[1m"
|
|
25
|
+
DIM = "\033[2m"
|
|
26
|
+
GREEN = "\033[32m"
|
|
27
|
+
CYAN = "\033[36m"
|
|
28
|
+
YELLOW = "\033[33m"
|
|
29
|
+
RED = "\033[31m"
|
|
30
|
+
RESET = "\033[0m"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def print_success(msg: str):
|
|
34
|
+
print(f"{GREEN}✔{RESET} {msg}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_error(msg: str):
|
|
38
|
+
print(f"{RED}✖ Error:{RESET} {msg}", file=sys.stderr)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def print_warning(msg: str):
|
|
42
|
+
print(f"{YELLOW}⚠{RESET} {msg}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def print_header(msg: str):
|
|
46
|
+
print(f"{BOLD}{CYAN}==>{RESET} {BOLD}{msg}{RESET}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cmd_login(args):
|
|
50
|
+
"""Log in with an API token."""
|
|
51
|
+
token = args.token
|
|
52
|
+
if not token:
|
|
53
|
+
print(f"\n{BOLD}LiteLambda Authentication{RESET}")
|
|
54
|
+
print(f"Generate your personal API token at: {CYAN}https://litelambda.in/settings/#api-tokens{RESET}\n")
|
|
55
|
+
try:
|
|
56
|
+
token = input(f"{BOLD}Enter your API token (ll_live_...): {RESET}").strip()
|
|
57
|
+
except (KeyboardInterrupt, EOFError):
|
|
58
|
+
print("\nCancelled.")
|
|
59
|
+
sys.exit(1)
|
|
60
|
+
|
|
61
|
+
if not token:
|
|
62
|
+
print_error("Token cannot be empty.")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
client = LiteLambdaClient(token=token)
|
|
66
|
+
try:
|
|
67
|
+
data = client.verify_auth()
|
|
68
|
+
user = data.get("user", {})
|
|
69
|
+
email = user.get("email")
|
|
70
|
+
plan = user.get("plan", "").title()
|
|
71
|
+
save_credentials(token, user_email=email)
|
|
72
|
+
print_success(f"Logged in successfully as {BOLD}{email}{RESET} ({plan} plan).")
|
|
73
|
+
print(f"Credentials stored in {DIM}~/.litelambda/credentials.json{RESET}")
|
|
74
|
+
except APIError as e:
|
|
75
|
+
print_error(f"Authentication failed: {e}")
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cmd_whoami(args):
|
|
80
|
+
"""Show current logged-in user and account stats."""
|
|
81
|
+
client = LiteLambdaClient()
|
|
82
|
+
if not client.token:
|
|
83
|
+
print_error("Not logged in. Run `litelambda login` first.")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
data = client.verify_auth()
|
|
88
|
+
u = data.get("user", {})
|
|
89
|
+
t = data.get("token", {})
|
|
90
|
+
print(f"\n{BOLD}LiteLambda Account{RESET}")
|
|
91
|
+
print(f" {DIM}Email:{RESET} {BOLD}{u.get('email')}{RESET}")
|
|
92
|
+
print(f" {DIM}Plan:{RESET} {u.get('plan', '').title()}")
|
|
93
|
+
print(f" {DIM}Credits Left:{RESET} {GREEN}{u.get('credits')}{RESET}")
|
|
94
|
+
print(f" {DIM}Active Crons:{RESET} {u.get('active_crons')}")
|
|
95
|
+
if u.get('trial_days_remaining') is not None:
|
|
96
|
+
print(f" {DIM}Trial Days:{RESET} {YELLOW}{u.get('trial_days_remaining')} days left{RESET}")
|
|
97
|
+
print(f" {DIM}Using Token:{RESET} {t.get('name')} ({t.get('prefix')})")
|
|
98
|
+
print()
|
|
99
|
+
except APIError as e:
|
|
100
|
+
print_error(str(e))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def cmd_list(args):
|
|
105
|
+
"""List all deployed cron jobs."""
|
|
106
|
+
client = LiteLambdaClient()
|
|
107
|
+
if not client.token:
|
|
108
|
+
print_error("Not logged in. Run `litelambda login` first.")
|
|
109
|
+
sys.exit(1)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
data = client.list_crons()
|
|
113
|
+
crons = data.get("crons", [])
|
|
114
|
+
if not crons:
|
|
115
|
+
print(f"No cron jobs found. Deploy your first job with: {CYAN}litelambda deploy <file.py>{RESET}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
print(f"\n{BOLD}Deployed Cron Jobs ({len(crons)}){RESET}")
|
|
119
|
+
# Format table
|
|
120
|
+
header = f"{'NAME':<24} {'SCHEDULE':<16} {'STATUS':<10} {'TIMEOUT':<9} {'LAST RUN':<20}"
|
|
121
|
+
print(f"{DIM}{header}{RESET}")
|
|
122
|
+
print(f"{DIM}{'-' * 80}{RESET}")
|
|
123
|
+
for c in crons:
|
|
124
|
+
status = c.get('status', '').upper()
|
|
125
|
+
status_color = GREEN if status == 'ACTIVE' else YELLOW
|
|
126
|
+
last_run = c.get('last_run_at')
|
|
127
|
+
last_run_str = last_run[:19].replace('T', ' ') if last_run else "Never"
|
|
128
|
+
dur = f" ({c['last_duration_ms']}ms)" if c.get('last_duration_ms') else ""
|
|
129
|
+
print(f"{BOLD}{c['name']:<24}{RESET} {c['schedule']:<16} {status_color}{status:<10}{RESET} {str(c['timeout']) + 's':<9} {last_run_str + dur}")
|
|
130
|
+
print()
|
|
131
|
+
except APIError as e:
|
|
132
|
+
print_error(str(e))
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_deploy(args):
|
|
137
|
+
"""Deploy a Python script to LiteLambda."""
|
|
138
|
+
client = LiteLambdaClient()
|
|
139
|
+
if not client.token:
|
|
140
|
+
print_error("Not logged in. Run `litelambda login` first.")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
filepath = Path(args.file)
|
|
144
|
+
if not filepath.exists():
|
|
145
|
+
print_error(f"File not found: {filepath}")
|
|
146
|
+
sys.exit(1)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
code = filepath.read_text(encoding="utf-8")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print_error(f"Failed to read file: {e}")
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
name = args.name or filepath.stem
|
|
155
|
+
packages = ""
|
|
156
|
+
if args.requirements:
|
|
157
|
+
req_path = Path(args.requirements)
|
|
158
|
+
if not req_path.exists():
|
|
159
|
+
print_error(f"Requirements file not found: {req_path}")
|
|
160
|
+
sys.exit(1)
|
|
161
|
+
packages = req_path.read_text(encoding="utf-8")
|
|
162
|
+
|
|
163
|
+
env_vars = ""
|
|
164
|
+
if args.env:
|
|
165
|
+
env_path = Path(args.env)
|
|
166
|
+
if env_path.exists():
|
|
167
|
+
env_vars = env_path.read_text(encoding="utf-8")
|
|
168
|
+
else:
|
|
169
|
+
# Assume inline KEY=VALUE pairs separated by comma or newline
|
|
170
|
+
env_vars = args.env.replace(",", "\n")
|
|
171
|
+
|
|
172
|
+
payload = {
|
|
173
|
+
"name": name,
|
|
174
|
+
"code": code,
|
|
175
|
+
"schedule": args.schedule,
|
|
176
|
+
"packages": packages,
|
|
177
|
+
"env_vars": env_vars,
|
|
178
|
+
"timeout": args.timeout,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
print_header(f"Deploying '{name}' to LiteLambda...")
|
|
182
|
+
try:
|
|
183
|
+
res = client.deploy_cron(payload)
|
|
184
|
+
action = res.get("action", "deployed")
|
|
185
|
+
cron = res.get("cron", {})
|
|
186
|
+
print_success(f"Job successfully {action}!")
|
|
187
|
+
print(f" {DIM}ID:{RESET} {cron.get('id')}")
|
|
188
|
+
print(f" {DIM}Schedule:{RESET} {cron.get('schedule')}")
|
|
189
|
+
print(f" {DIM}Timeout:{RESET} {cron.get('timeout')}s")
|
|
190
|
+
print(f" {DIM}Web URL:{RESET} {CYAN}{cron.get('web_url')}{RESET}\n")
|
|
191
|
+
except APIError as e:
|
|
192
|
+
print_error(str(e))
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def cmd_run(args):
|
|
197
|
+
"""Trigger manual execution of a cron job."""
|
|
198
|
+
client = LiteLambdaClient()
|
|
199
|
+
if not client.token:
|
|
200
|
+
print_error("Not logged in. Run `litelambda login` first.")
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
|
|
203
|
+
id_or_name = args.cron
|
|
204
|
+
print_header(f"Triggering run for '{id_or_name}'...")
|
|
205
|
+
try:
|
|
206
|
+
res = client.run_cron(id_or_name)
|
|
207
|
+
print_success(res.get("message", "Job triggered."))
|
|
208
|
+
print(f"Check execution logs with: {CYAN}litelambda logs \"{id_or_name}\"{RESET}")
|
|
209
|
+
except APIError as e:
|
|
210
|
+
print_error(str(e))
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def cmd_logs(args):
|
|
215
|
+
"""View execution logs for a cron job."""
|
|
216
|
+
client = LiteLambdaClient()
|
|
217
|
+
if not client.token:
|
|
218
|
+
print_error("Not logged in. Run `litelambda login` first.")
|
|
219
|
+
sys.exit(1)
|
|
220
|
+
|
|
221
|
+
id_or_name = args.cron
|
|
222
|
+
try:
|
|
223
|
+
data = client.get_logs(id_or_name)
|
|
224
|
+
executions = data.get("executions", [])
|
|
225
|
+
if not executions:
|
|
226
|
+
print(f"No execution history recorded for '{id_or_name}'.")
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
latest = executions[0]
|
|
230
|
+
status = latest.get("status", "").upper()
|
|
231
|
+
status_color = GREEN if status == "SUCCESS" else (RED if status == "ERROR" else YELLOW)
|
|
232
|
+
|
|
233
|
+
print(f"\n{BOLD}Execution Logs for '{data.get('cron_name')}'{RESET}")
|
|
234
|
+
print(f" {DIM}Started At:{RESET} {latest.get('started_at')}")
|
|
235
|
+
print(f" {DIM}Duration:{RESET} {latest.get('duration_ms')} ms")
|
|
236
|
+
print(f" {DIM}Status:{RESET} {status_color}{status}{RESET}\n")
|
|
237
|
+
|
|
238
|
+
print(f"{DIM}{'─' * 60}{RESET}")
|
|
239
|
+
log_lines = latest.get("log_lines") or []
|
|
240
|
+
if log_lines:
|
|
241
|
+
for l in log_lines:
|
|
242
|
+
t = l.get("t", "")[11:19]
|
|
243
|
+
msg = l.get("msg", "")
|
|
244
|
+
level = l.get("level", "info")
|
|
245
|
+
color = RED if level == "error" else RESET
|
|
246
|
+
print(f"{DIM}[{t}]{RESET} {color}{msg}{RESET}")
|
|
247
|
+
elif latest.get("error_message"):
|
|
248
|
+
print(f"{RED}{latest['error_message']}{RESET}")
|
|
249
|
+
else:
|
|
250
|
+
print(f"{DIM}(Execution finished with no console output){RESET}")
|
|
251
|
+
print(f"{DIM}{'─' * 60}{RESET}\n")
|
|
252
|
+
|
|
253
|
+
except APIError as e:
|
|
254
|
+
print_error(str(e))
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def main():
|
|
259
|
+
parser = argparse.ArgumentParser(
|
|
260
|
+
prog="litelambda",
|
|
261
|
+
description="Official CLI for LiteLambda (https://litelambda.in) — Serverless Python Cron Hosting",
|
|
262
|
+
)
|
|
263
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to run")
|
|
264
|
+
|
|
265
|
+
# login
|
|
266
|
+
p_login = subparsers.add_parser("login", help="Authenticate with an API token")
|
|
267
|
+
p_login.add_argument("--token", "-t", help="API token (ll_live_...)")
|
|
268
|
+
p_login.set_defaults(func=cmd_login)
|
|
269
|
+
|
|
270
|
+
# whoami
|
|
271
|
+
p_whoami = subparsers.add_parser("whoami", help="Show current logged-in account")
|
|
272
|
+
p_whoami.set_defaults(func=cmd_whoami)
|
|
273
|
+
|
|
274
|
+
# list
|
|
275
|
+
p_list = subparsers.add_parser("list", help="List all deployed cron jobs")
|
|
276
|
+
p_list.set_defaults(func=cmd_list)
|
|
277
|
+
|
|
278
|
+
# deploy
|
|
279
|
+
p_deploy = subparsers.add_parser("deploy", help="Deploy a Python script on schedule")
|
|
280
|
+
p_deploy.add_argument("file", help="Path to Python script (.py)")
|
|
281
|
+
p_deploy.add_argument("--schedule", "-s", default="*/5 * * * *", help="Cron schedule expression (default: '*/5 * * * *')")
|
|
282
|
+
p_deploy.add_argument("--name", "-n", help="Cron job name (defaults to filename)")
|
|
283
|
+
p_deploy.add_argument("--requirements", "-r", help="Path to requirements.txt")
|
|
284
|
+
p_deploy.add_argument("--env", "-e", help="Path to .env file or comma-separated KEY=VALUE pairs")
|
|
285
|
+
p_deploy.add_argument("--timeout", type=int, default=30, help="Execution timeout in seconds (default: 30)")
|
|
286
|
+
p_deploy.set_defaults(func=cmd_deploy)
|
|
287
|
+
|
|
288
|
+
# run
|
|
289
|
+
p_run = subparsers.add_parser("run", help="Trigger a cron job execution manually")
|
|
290
|
+
p_run.add_argument("cron", help="Name or UUID of the cron job")
|
|
291
|
+
p_run.set_defaults(func=cmd_run)
|
|
292
|
+
|
|
293
|
+
# logs
|
|
294
|
+
p_logs = subparsers.add_parser("logs", help="View recent execution logs")
|
|
295
|
+
p_logs.add_argument("cron", help="Name or UUID of the cron job")
|
|
296
|
+
p_logs.set_defaults(func=cmd_logs)
|
|
297
|
+
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
if not args.command:
|
|
300
|
+
parser.print_help()
|
|
301
|
+
sys.exit(0)
|
|
302
|
+
|
|
303
|
+
args.func(args)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
if __name__ == "__main__":
|
|
307
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: litelambda-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official CLI for LiteLambda — Serverless Python Cron Job Platform
|
|
5
|
+
Author-email: LiteLambda <support@litelambda.in>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://litelambda.in
|
|
8
|
+
Project-URL: Documentation, https://litelambda.in/docs
|
|
9
|
+
Project-URL: Repository, https://github.com/litelambda/litelambda-cli
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# LiteLambda CLI (`litelambda`)
|
|
18
|
+
|
|
19
|
+
Official command-line interface for [LiteLambda](https://litelambda.in) — serverless Python cron job hosting without managing servers.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install litelambda-cli
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### 1. Login
|
|
30
|
+
Generate an API token from your [LiteLambda Settings](https://litelambda.in/settings/#api-tokens), then run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
litelambda login
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Check Account
|
|
37
|
+
```bash
|
|
38
|
+
litelambda whoami
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 3. Deploy a Python Script
|
|
42
|
+
Deploy any Python file on a cron schedule:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
litelambda deploy scraper.py --schedule "0 9 * * *" --name "Morning Scraper"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
With pip dependencies:
|
|
49
|
+
```bash
|
|
50
|
+
litelambda deploy bot.py --schedule "*/15 * * * *" --requirements requirements.txt
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 4. List All Jobs
|
|
54
|
+
```bash
|
|
55
|
+
litelambda list
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 5. Run a Job Manually
|
|
59
|
+
```bash
|
|
60
|
+
litelambda run "Morning Scraper"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 6. View Execution Logs
|
|
64
|
+
```bash
|
|
65
|
+
litelambda logs "Morning Scraper"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
MIT
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
litelambda/__init__.py
|
|
4
|
+
litelambda/client.py
|
|
5
|
+
litelambda/config.py
|
|
6
|
+
litelambda/main.py
|
|
7
|
+
litelambda_cli.egg-info/PKG-INFO
|
|
8
|
+
litelambda_cli.egg-info/SOURCES.txt
|
|
9
|
+
litelambda_cli.egg-info/dependency_links.txt
|
|
10
|
+
litelambda_cli.egg-info/entry_points.txt
|
|
11
|
+
litelambda_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
litelambda
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "litelambda-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="LiteLambda", email="support@litelambda.in" },
|
|
10
|
+
]
|
|
11
|
+
description = "Official CLI for LiteLambda — Serverless Python Cron Job Platform"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
requires-python = ">=3.8"
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Topic :: Software Development :: Build Tools",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://litelambda.in"
|
|
24
|
+
Documentation = "https://litelambda.in/docs"
|
|
25
|
+
Repository = "https://github.com/litelambda/litelambda-cli"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
litelambda = "litelambda.main:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["litelambda*"]
|