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.
- streamlift_worker-1.1.0/MANIFEST.in +17 -0
- streamlift_worker-1.1.0/PKG-INFO +72 -0
- streamlift_worker-1.1.0/PUBLISHING.md +104 -0
- streamlift_worker-1.1.0/README.md +58 -0
- streamlift_worker-1.1.0/pyproject.toml +43 -0
- streamlift_worker-1.1.0/setup.cfg +4 -0
- streamlift_worker-1.1.0/streamlift_worker/__init__.py +8 -0
- streamlift_worker-1.1.0/streamlift_worker/__main__.py +72 -0
- streamlift_worker-1.1.0/streamlift_worker/api.py +148 -0
- streamlift_worker-1.1.0/streamlift_worker/config.py +38 -0
- streamlift_worker-1.1.0/streamlift_worker/downloader.py +402 -0
- streamlift_worker-1.1.0/streamlift_worker/logger.py +34 -0
- streamlift_worker-1.1.0/streamlift_worker/mega.py +178 -0
- streamlift_worker-1.1.0/streamlift_worker/metrics.py +46 -0
- streamlift_worker-1.1.0/streamlift_worker/worker.py +78 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/PKG-INFO +72 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/SOURCES.txt +21 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/dependency_links.txt +1 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/entry_points.txt +2 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/requires.txt +8 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/scm_file_list.json +18 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/scm_version.json +8 -0
- streamlift_worker-1.1.0/streamlift_worker.egg-info/top_level.txt +1 -0
|
@@ -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,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"]
|