streamlift-worker 1.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.
@@ -0,0 +1,17 @@
1
+ # Only include what belongs in the published source distribution.
2
+ # Everything not listed here is excluded — the rest of the monorepo never touches PyPI.
3
+
4
+ include pyproject.toml
5
+ include README.md
6
+ include MANIFEST.in
7
+
8
+ recursive-include streamlift_worker *.py
9
+
10
+ # Explicitly exclude everything else (belt-and-suspenders)
11
+ exclude .env
12
+ exclude .env.example
13
+ exclude .gitignore
14
+ prune dist
15
+ prune build
16
+ prune .github
17
+ prune downloads
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: streamlift-worker
3
+ Version: 1.1.0
4
+ Summary: StreamLift distributed download worker — runs in Google Colab
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: requests==2.32.3
8
+ Requires-Dist: psutil==6.1.1
9
+ Provides-Extra: dev
10
+ Requires-Dist: ruff==0.9.10; extra == "dev"
11
+ Requires-Dist: pytest==8.4.1; extra == "dev"
12
+ Requires-Dist: build==1.2.2; extra == "dev"
13
+ Requires-Dist: twine==6.1.0; extra == "dev"
14
+
15
+ # streamlift-worker
16
+
17
+ Distributed download worker for [StreamLift](https://github.com/shubhra92/StreamLift).
18
+ Designed to run in **Google Colab** — paste the one-liner from the dashboard and go.
19
+
20
+ ## What it does
21
+
22
+ - Registers itself with the StreamLift backend
23
+ - Polls for download tasks (HTTP URLs or magnet links)
24
+ - Downloads files and streams them directly to **Mega** (zero local disk) or saves them **locally**
25
+ - Reports live progress back to the backend
26
+ - Sends heartbeats with CPU / RAM / network metrics
27
+
28
+ ## Install & run (Colab cell)
29
+
30
+ The StreamLift UI generates this for you with all credentials pre-filled:
31
+
32
+ ```python
33
+ !pip install -q requests psutil
34
+ !pip install -q git+https://github.com/shubhra92/megapy.git
35
+ !pip install -q git+https://github.com/shubhra92/streamlift-worker.git
36
+
37
+ !streamlift-worker \
38
+ --worker-id "your-worker-uuid" \
39
+ --auth-token "your-auth-token" \
40
+ --api-url "https://your-app.com" \
41
+ --compute-type "medium" \
42
+ --location "mega" \
43
+ --mega-email "you@example.com" \
44
+ --mega-password "yourpassword"
45
+ ```
46
+
47
+ ## CLI flags
48
+
49
+ | Flag | Required | Description |
50
+ |------|----------|-------------|
51
+ | `--worker-id` | ✅ | Worker UUID from the StreamLift dashboard |
52
+ | `--auth-token` | ✅ | Auth token for this worker |
53
+ | `--api-url` | ✅ | StreamLift backend base URL |
54
+ | `--compute-type` | ✅ | Resource profile: `low` \| `medium` \| `high` |
55
+ | `--location` | ✅ | Where to store files: `local` \| `mega` |
56
+ | `--mega-email` | ⚠️ | Required when `--location=mega` |
57
+ | `--mega-password` | ⚠️ | Required when `--location=mega` |
58
+
59
+ ## Package structure
60
+
61
+ ```
62
+ streamlift_worker/
63
+ ├── __init__.py # version
64
+ ├── __main__.py # CLI entry point & arg parsing
65
+ ├── config.py # WorkerConfig dataclass
66
+ ├── worker.py # main loop, registration, heartbeat thread
67
+ ├── downloader.py # HTTP + torrent download handlers
68
+ ├── mega.py # Mega upload helpers (stream & file)
69
+ ├── api.py # all HTTP calls to the backend
70
+ ├── metrics.py # CPU / RAM / network metrics
71
+ └── logger.py # in-process log queue
72
+ ```
@@ -0,0 +1,104 @@
1
+ # Publishing Guide — streamlift-worker
2
+
3
+ ## One-time PyPI Trusted Publishing setup
4
+
5
+ Do this once. You never need to store an API token anywhere.
6
+
7
+ 1. Log in to [pypi.org](https://pypi.org)
8
+ 2. Go to **Your projects → streamlift-worker → Manage → Publishing**
9
+ (If the project doesn't exist yet, go to **Account settings → Publishing** and add a pending publisher)
10
+ 3. Click **Add a new publisher** and fill in:
11
+
12
+ | Field | Value |
13
+ |-------|-------|
14
+ | PyPI project name | `streamlift-worker` |
15
+ | Owner | your GitHub username |
16
+ | Repository | `StreamLift` (your private repo name) |
17
+ | Workflow filename | `publish-python-worker.yml` |
18
+ | Environment name | `pypi` |
19
+
20
+ 4. On GitHub, go to **Settings → Environments → New environment** and name it `pypi`.
21
+ No secrets needed — OIDC handles authentication automatically.
22
+
23
+ ---
24
+
25
+ ## How to release a new version
26
+
27
+ ```bash
28
+ # 1. Make sure your changes are committed and on main
29
+ git checkout main
30
+ git pull
31
+
32
+ # 2. Tag the release — this is what drives the version number
33
+ git tag v1.2.0
34
+
35
+ # 3. Push the tag — this triggers the GitHub Actions workflow
36
+ git push origin v1.2.0
37
+ ```
38
+
39
+ That's it. GitHub Actions will:
40
+ - Check out the repo with full history
41
+ - Build the wheel + sdist from `python-worker/` only
42
+ - Publish to PyPI via OIDC (no token needed)
43
+
44
+ The package will be live at `https://pypi.org/project/streamlift-worker/1.2.0/`
45
+ within ~2 minutes of pushing the tag.
46
+
47
+ ---
48
+
49
+ ## Version numbering
50
+
51
+ The version is read directly from the Git tag by `setuptools-scm`:
52
+
53
+ | Git tag | PyPI version |
54
+ |---------|-------------|
55
+ | `v1.0.0` | `1.0.0` |
56
+ | `v1.2.3` | `1.2.3` |
57
+ | `v2.0.0-rc1` | `2.0.0rc1` |
58
+
59
+ **Do not set `version =` in `pyproject.toml`** — it's `dynamic` and will be wrong.
60
+
61
+ ---
62
+
63
+ ## Test locally before releasing
64
+
65
+ ```bash
66
+ cd python-worker
67
+
68
+ # Create a clean virtual environment
69
+ python -m venv .venv
70
+ source .venv/bin/activate
71
+
72
+ # Install in editable mode with dev tools
73
+ pip install -e ".[dev]"
74
+
75
+ # Run a lint check
76
+ ruff check streamlift_worker/
77
+
78
+ # Build the package locally (output goes to dist/)
79
+ python -m build
80
+
81
+ # Inspect what's inside the wheel — make sure only streamlift_worker/ is there
82
+ unzip -l dist/streamlift_worker-*.whl
83
+
84
+ # Optional: do a dry-run upload to Test PyPI first
85
+ pip install twine
86
+ twine upload --repository testpypi dist/*
87
+ # Then test install from Test PyPI:
88
+ pip install --index-url https://test.pypi.org/simple/ streamlift-worker
89
+ ```
90
+
91
+ ---
92
+
93
+ ## What's public vs private
94
+
95
+ | What | Visibility |
96
+ |------|-----------|
97
+ | Your GitHub repo (`StreamLift/`) | **Private** — only you |
98
+ | `express-backend/`, `next-frontend/`, etc. | **Private** — never leaves GitHub |
99
+ | `python-worker/streamlift_worker/*.py` | **Public** — included in the PyPI wheel |
100
+ | `python-worker/README.md`, `pyproject.toml` | **Public** — included in the sdist |
101
+ | `.env`, credentials, other configs | **Private** — excluded by `MANIFEST.in` |
102
+
103
+ > PyPI publishes only the built distribution files (`.whl` + `.tar.gz`).
104
+ > It has no access to your GitHub repository at all.
@@ -0,0 +1,58 @@
1
+ # streamlift-worker
2
+
3
+ Distributed download worker for [StreamLift](https://github.com/shubhra92/StreamLift).
4
+ Designed to run in **Google Colab** — paste the one-liner from the dashboard and go.
5
+
6
+ ## What it does
7
+
8
+ - Registers itself with the StreamLift backend
9
+ - Polls for download tasks (HTTP URLs or magnet links)
10
+ - Downloads files and streams them directly to **Mega** (zero local disk) or saves them **locally**
11
+ - Reports live progress back to the backend
12
+ - Sends heartbeats with CPU / RAM / network metrics
13
+
14
+ ## Install & run (Colab cell)
15
+
16
+ The StreamLift UI generates this for you with all credentials pre-filled:
17
+
18
+ ```python
19
+ !pip install -q requests psutil
20
+ !pip install -q git+https://github.com/shubhra92/megapy.git
21
+ !pip install -q git+https://github.com/shubhra92/streamlift-worker.git
22
+
23
+ !streamlift-worker \
24
+ --worker-id "your-worker-uuid" \
25
+ --auth-token "your-auth-token" \
26
+ --api-url "https://your-app.com" \
27
+ --compute-type "medium" \
28
+ --location "mega" \
29
+ --mega-email "you@example.com" \
30
+ --mega-password "yourpassword"
31
+ ```
32
+
33
+ ## CLI flags
34
+
35
+ | Flag | Required | Description |
36
+ |------|----------|-------------|
37
+ | `--worker-id` | ✅ | Worker UUID from the StreamLift dashboard |
38
+ | `--auth-token` | ✅ | Auth token for this worker |
39
+ | `--api-url` | ✅ | StreamLift backend base URL |
40
+ | `--compute-type` | ✅ | Resource profile: `low` \| `medium` \| `high` |
41
+ | `--location` | ✅ | Where to store files: `local` \| `mega` |
42
+ | `--mega-email` | ⚠️ | Required when `--location=mega` |
43
+ | `--mega-password` | ⚠️ | Required when `--location=mega` |
44
+
45
+ ## Package structure
46
+
47
+ ```
48
+ streamlift_worker/
49
+ ├── __init__.py # version
50
+ ├── __main__.py # CLI entry point & arg parsing
51
+ ├── config.py # WorkerConfig dataclass
52
+ ├── worker.py # main loop, registration, heartbeat thread
53
+ ├── downloader.py # HTTP + torrent download handlers
54
+ ├── mega.py # Mega upload helpers (stream & file)
55
+ ├── api.py # all HTTP calls to the backend
56
+ ├── metrics.py # CPU / RAM / network metrics
57
+ └── logger.py # in-process log queue
58
+ ```
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "setuptools-scm>=8", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "streamlift-worker"
7
+ # Version is derived automatically from the Git tag (e.g. v1.1.0 → 1.1.0).
8
+ # Do NOT set version here manually — setuptools-scm handles it.
9
+ dynamic = ["version"]
10
+ description = "StreamLift distributed download worker — runs in Google Colab"
11
+ readme = "README.md"
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "requests==2.32.3",
15
+ "psutil==6.1.1",
16
+ # mega.py fork with upload_stream support:
17
+ # pip install git+https://github.com/shubhra92/megapy.git
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ dev = [
22
+ "ruff==0.9.10",
23
+ "pytest==8.4.1",
24
+ "build==1.2.2",
25
+ "twine==6.1.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ streamlift-worker = "streamlift_worker.__main__:main"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["."]
33
+ include = ["streamlift_worker*"]
34
+
35
+ # setuptools-scm: read version from the nearest Git tag
36
+ # root ".." because pyproject.toml lives in python-worker/ but .git is at repo root
37
+ [tool.setuptools_scm]
38
+ root = ".."
39
+ write_to = "streamlift_worker/_version.py"
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py311"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """StreamLift Worker — distributed download agent for Google Colab."""
2
+
3
+ try:
4
+ # Populated by setuptools-scm at build time from the Git tag.
5
+ from streamlift_worker._version import version as __version__
6
+ except ImportError:
7
+ # Running from source without a build (e.g. direct git clone).
8
+ __version__ = "0.0.0.dev0"
@@ -0,0 +1,72 @@
1
+ """
2
+ CLI entry point.
3
+
4
+ Usage (from PyPI / GitHub):
5
+ streamlift-worker \\
6
+ --worker-id "uuid" \\
7
+ --auth-token "token" \\
8
+ --api-url "https://your-app.com" \\
9
+ --compute-type "medium" \\
10
+ --location "mega" \\
11
+ --mega-email "you@example.com" \\
12
+ --mega-password "secret"
13
+
14
+ Or run as a module:
15
+ python -m streamlift_worker ...same flags...
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+
21
+ from streamlift_worker.config import WorkerConfig
22
+ from streamlift_worker.worker import run
23
+
24
+
25
+ def _parse_args() -> WorkerConfig:
26
+ p = argparse.ArgumentParser(
27
+ prog="streamlift-worker",
28
+ description="StreamLift distributed download worker",
29
+ )
30
+
31
+ p.add_argument("--worker-id", required=True, help="Worker UUID from StreamLift dashboard")
32
+ p.add_argument("--auth-token", required=True, help="Auth token for this worker")
33
+ p.add_argument("--api-url", required=True, help="StreamLift backend base URL (e.g. https://app.streamlift.io)")
34
+ p.add_argument("--compute-type", required=True, choices=["low", "medium", "high"],
35
+ help="Resource profile: low | medium | high")
36
+ p.add_argument("--location", required=True, choices=["local", "mega"],
37
+ dest="download_location", help="Where to store downloaded files: local | mega")
38
+ p.add_argument("--mega-email", default="", help="Mega account email (required when --location=mega)")
39
+ p.add_argument("--mega-password", default="", help="Mega account password (required when --location=mega)")
40
+ p.add_argument("--version", action="version", version=f"%(prog)s {_get_version()}")
41
+
42
+ args = p.parse_args()
43
+
44
+ if args.download_location == "mega" and not (args.mega_email and args.mega_password):
45
+ p.error("--mega-email and --mega-password are required when --location=mega")
46
+
47
+ return WorkerConfig(
48
+ worker_id= args.worker_id,
49
+ auth_token= args.auth_token,
50
+ api_base_url= args.api_url.rstrip("/"),
51
+ compute_type= args.compute_type,
52
+ download_location= args.download_location,
53
+ mega_email= args.mega_email,
54
+ mega_password= args.mega_password,
55
+ )
56
+
57
+
58
+ def _get_version() -> str:
59
+ try:
60
+ from streamlift_worker import __version__
61
+ return __version__
62
+ except Exception:
63
+ return "unknown"
64
+
65
+
66
+ def main() -> None:
67
+ config = _parse_args()
68
+ run(config)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
@@ -0,0 +1,148 @@
1
+ """
2
+ HTTP communication with the StreamLift backend.
3
+ All outbound calls live here — the rest of the package never calls requests directly.
4
+ """
5
+
6
+ import sys
7
+ import threading
8
+ import time
9
+ from typing import Any, Optional
10
+
11
+ import requests
12
+
13
+ from streamlift_worker import logger
14
+ from streamlift_worker.config import MAX_RETRIES, RETRY_DELAY, WorkerConfig
15
+
16
+ # Shared stop event — set by the auth-failure handler so every loop exits cleanly
17
+ stop_event = threading.Event()
18
+
19
+
20
+ def _post(config: WorkerConfig, endpoint: str, data: dict[str, Any]) -> Optional[dict]:
21
+ url = f"{config.api_base_url}{endpoint}"
22
+ headers = {"Content-Type": "application/json"}
23
+
24
+ for attempt in range(1, MAX_RETRIES + 1):
25
+ try:
26
+ r = requests.post(url, json=data, headers=headers, timeout=30)
27
+
28
+ if r.status_code == 401:
29
+ logger.log("error", "Authentication failed — invalid worker ID or auth token. Stopping.")
30
+ stop_event.set()
31
+ sys.exit(1)
32
+
33
+ if r.status_code >= 500:
34
+ raise requests.RequestException(f"Server error {r.status_code}")
35
+
36
+ return r.json()
37
+
38
+ except requests.RequestException as e:
39
+ logger.log("warning", f"POST {endpoint} failed (attempt {attempt}/{MAX_RETRIES}): {e}")
40
+ if attempt < MAX_RETRIES:
41
+ time.sleep(RETRY_DELAY * attempt)
42
+
43
+ logger.log("error", f"POST {endpoint} gave up after {MAX_RETRIES} attempts")
44
+ return None
45
+
46
+
47
+ def _get(config: WorkerConfig, endpoint: str, params: dict[str, Any] | None = None) -> Optional[dict]:
48
+ url = f"{config.api_base_url}{endpoint}"
49
+ try:
50
+ r = requests.get(url, params=params, timeout=15)
51
+ if r.status_code == 200:
52
+ return r.json()
53
+ except requests.RequestException as e:
54
+ logger.log("warning", f"GET {endpoint} failed: {e}")
55
+ return None
56
+
57
+
58
+ # ── Worker lifecycle ──────────────────────────────────────────────────────────
59
+
60
+ def register(config: WorkerConfig, ip_address: str) -> bool:
61
+ result = _post(config, "/api/worker/register", {
62
+ "workerId": config.worker_id,
63
+ "authToken": config.auth_token,
64
+ "ipAddress": ip_address,
65
+ "version": config.worker_version,
66
+ })
67
+ if result and result.get("success"):
68
+ logger.log("info", f"Registered successfully. Public IP: {ip_address}")
69
+ return True
70
+ logger.log("error", f"Registration failed. Response: {result}")
71
+ return False
72
+
73
+
74
+ def heartbeat(config: WorkerConfig, current_task: Optional[dict], metrics: dict) -> Optional[dict]:
75
+ payload: dict[str, Any] = {
76
+ "workerId": config.worker_id,
77
+ "authToken": config.auth_token,
78
+ "metrics": metrics,
79
+ }
80
+ if current_task:
81
+ payload["currentTask"] = current_task
82
+ return _post(config, "/api/worker/heartbeat", payload)
83
+
84
+
85
+ def flush_logs(config: WorkerConfig) -> None:
86
+ if logger.pending_count() == 0:
87
+ return
88
+ batch = logger.pop_batch(10)
89
+ result = _post(config, "/api/worker/logs", {
90
+ "workerId": config.worker_id,
91
+ "authToken": config.auth_token,
92
+ "logs": batch,
93
+ })
94
+ # If the call failed, put the batch back at the front so we retry next time
95
+ if not (result and result.get("success")):
96
+ from streamlift_worker.logger import _queue
97
+ _queue[:0] = batch
98
+
99
+
100
+ def report_progress(
101
+ config: WorkerConfig,
102
+ download_id: str,
103
+ downloaded: int,
104
+ total: int,
105
+ status: str,
106
+ file_name: str = "",
107
+ error_msg: str = "",
108
+ ) -> None:
109
+ pct = round(downloaded / total * 100, 2) if total > 0 else 0
110
+ payload: dict[str, Any] = {
111
+ "workerId": config.worker_id,
112
+ "authToken": config.auth_token,
113
+ "downloadId": download_id,
114
+ "progress": {
115
+ "downloadedBytes": downloaded,
116
+ "totalBytes": total,
117
+ "percent": pct,
118
+ "status": status,
119
+ },
120
+ }
121
+ if error_msg:
122
+ payload["progress"]["errorMessage"] = error_msg
123
+ _post(config, "/api/worker/download-progress", payload)
124
+
125
+
126
+ # ── Mega session persistence ──────────────────────────────────────────────────
127
+
128
+ def save_mega_session(config: WorkerConfig, session_id: str) -> None:
129
+ try:
130
+ _post(config, "/api/worker/mega-session", {
131
+ "workerId": config.worker_id,
132
+ "authToken": config.auth_token,
133
+ "email": config.mega_email,
134
+ "sessionData": session_id,
135
+ })
136
+ logger.log("info", "Mega session saved to backend")
137
+ except Exception as e:
138
+ logger.log("warning", f"Could not save Mega session: {e}")
139
+
140
+
141
+ def load_mega_session(config: WorkerConfig) -> Optional[str]:
142
+ data = _get(config, "/api/worker/mega-session", {
143
+ "workerId": config.worker_id,
144
+ "authToken": config.auth_token,
145
+ })
146
+ if data and data.get("success") and data.get("sessionData"):
147
+ return data["sessionData"]
148
+ return None
@@ -0,0 +1,38 @@
1
+ """
2
+ Worker configuration — populated entirely from CLI args.
3
+ No .env file needed; the user just runs the one-liner from the UI.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Literal
8
+
9
+ ComputeType = Literal["low", "medium", "high"]
10
+ DownloadLocation = Literal["local", "mega"]
11
+
12
+ COMPUTE_CONFIG = {
13
+ "low": {"max_cpu_pct": 25, "chunk_size": 512 * 1024}, # 512 KB
14
+ "medium": {"max_cpu_pct": 50, "chunk_size": 1024 * 1024}, # 1 MB
15
+ "high": {"max_cpu_pct": 100, "chunk_size": 2048 * 1024}, # 2 MB
16
+ }
17
+
18
+ POLL_INTERVAL = 10 # seconds between main-loop polls
19
+ MAX_RETRIES = 3
20
+ RETRY_DELAY = 5 # seconds, multiplied by attempt number
21
+ MAX_LOG_QUEUE = 50
22
+ HEARTBEAT_INTERVAL = 8 # background heartbeat thread interval (seconds)
23
+
24
+
25
+ @dataclass
26
+ class WorkerConfig:
27
+ worker_id: str
28
+ auth_token: str
29
+ api_base_url: str
30
+ compute_type: ComputeType
31
+ download_location: DownloadLocation
32
+ mega_email: str = ""
33
+ mega_password: str = ""
34
+ worker_version: str = "1.1.0"
35
+
36
+ @property
37
+ def chunk_size(self) -> int:
38
+ return COMPUTE_CONFIG[self.compute_type]["chunk_size"]