bossalabs 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.
- bossalabs-0.1.0/PKG-INFO +110 -0
- bossalabs-0.1.0/README.md +93 -0
- bossalabs-0.1.0/pyproject.toml +32 -0
- bossalabs-0.1.0/setup.cfg +4 -0
- bossalabs-0.1.0/src/bossalabs/__init__.py +29 -0
- bossalabs-0.1.0/src/bossalabs/cli/__init__.py +1 -0
- bossalabs-0.1.0/src/bossalabs/cli/main.py +95 -0
- bossalabs-0.1.0/src/bossalabs/client.py +254 -0
- bossalabs-0.1.0/src/bossalabs/constants.py +18 -0
- bossalabs-0.1.0/src/bossalabs/errors.py +24 -0
- bossalabs-0.1.0/src/bossalabs/models.py +33 -0
- bossalabs-0.1.0/src/bossalabs/scripts/__init__.py +0 -0
- bossalabs-0.1.0/src/bossalabs/scripts/upload_video.py +32 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/PKG-INFO +110 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/SOURCES.txt +18 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/dependency_links.txt +1 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/entry_points.txt +2 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/requires.txt +13 -0
- bossalabs-0.1.0/src/bossalabs.egg-info/top_level.txt +1 -0
- bossalabs-0.1.0/tests/test_client.py +23 -0
bossalabs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bossalabs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Requires-Dist: boto3
|
|
7
|
+
Requires-Dist: requests
|
|
8
|
+
Requires-Dist: fire
|
|
9
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
10
|
+
Requires-Dist: requests-toolbelt>=1.0.0
|
|
11
|
+
Requires-Dist: mypy
|
|
12
|
+
Requires-Dist: ruff
|
|
13
|
+
Requires-Dist: pytest
|
|
14
|
+
Requires-Dist: types-requests
|
|
15
|
+
Requires-Dist: tqdm>=4.67.3
|
|
16
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/computervisionsports/bossa-client/actions/workflows/ci.yml)
|
|
20
|
+
|
|
21
|
+
# BossaLabs Python Client
|
|
22
|
+
|
|
23
|
+
Small Python client for requesting upload authorization and uploading
|
|
24
|
+
video files using a presigned S3 POST.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv venv .venv
|
|
30
|
+
source .venv/bin/activate
|
|
31
|
+
uv pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or with plain pip:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from PyPI:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install bossalabs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Python usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from bossalabs import Client
|
|
50
|
+
|
|
51
|
+
client = Client(
|
|
52
|
+
api_key="YOUR_API_KEY",
|
|
53
|
+
model="MODEL_NAME"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = client.upload_video("/path/to/video.mp4")
|
|
57
|
+
print(result)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI usage
|
|
61
|
+
|
|
62
|
+
After installation, you can run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bossalabs process-video "/path/to/video.mp4" "MODEL_NAME" --api_key "YOUR_API_KEY"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
Full documentation is built with [MkDocs](https://www.mkdocs.org/) and
|
|
71
|
+
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).
|
|
72
|
+
|
|
73
|
+
### Install documentation dependencies
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync --group docs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Serve docs locally
|
|
80
|
+
|
|
81
|
+
Preview the site with live reload:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uv run mkdocs serve
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
|
|
88
|
+
|
|
89
|
+
### Build static HTML
|
|
90
|
+
|
|
91
|
+
Generate the static site into the `site/` directory:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run mkdocs build
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The HTML output is written to `site/` (gitignored). Deploy that folder to any
|
|
98
|
+
static hosting provider.
|
|
99
|
+
|
|
100
|
+
### What is documented
|
|
101
|
+
|
|
102
|
+
The docs cover the public user-facing API:
|
|
103
|
+
|
|
104
|
+
- `bossalabs.Client` — authorize and upload video files
|
|
105
|
+
- `bossalabs.UploadClientError` — authorization failure type
|
|
106
|
+
- `bossalabs.API_BASE_URL` — default API endpoint constant
|
|
107
|
+
- `bossalabs` CLI — `process-video` command
|
|
108
|
+
|
|
109
|
+
Internal modules (`bossalabs.models`, scripts, and private helpers) are
|
|
110
|
+
excluded from the API reference.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[](https://www.python.org/)
|
|
2
|
+
[](https://github.com/computervisionsports/bossa-client/actions/workflows/ci.yml)
|
|
3
|
+
|
|
4
|
+
# BossaLabs Python Client
|
|
5
|
+
|
|
6
|
+
Small Python client for requesting upload authorization and uploading
|
|
7
|
+
video files using a presigned S3 POST.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
uv venv .venv
|
|
13
|
+
source .venv/bin/activate
|
|
14
|
+
uv pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or with plain pip:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or from PyPI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install bossalabs
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Python usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from bossalabs import Client
|
|
33
|
+
|
|
34
|
+
client = Client(
|
|
35
|
+
api_key="YOUR_API_KEY",
|
|
36
|
+
model="MODEL_NAME"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
result = client.upload_video("/path/to/video.mp4")
|
|
40
|
+
print(result)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## CLI usage
|
|
44
|
+
|
|
45
|
+
After installation, you can run:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bossalabs process-video "/path/to/video.mp4" "MODEL_NAME" --api_key "YOUR_API_KEY"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Documentation
|
|
52
|
+
|
|
53
|
+
Full documentation is built with [MkDocs](https://www.mkdocs.org/) and
|
|
54
|
+
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).
|
|
55
|
+
|
|
56
|
+
### Install documentation dependencies
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
uv sync --group docs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Serve docs locally
|
|
63
|
+
|
|
64
|
+
Preview the site with live reload:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv run mkdocs serve
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
|
|
71
|
+
|
|
72
|
+
### Build static HTML
|
|
73
|
+
|
|
74
|
+
Generate the static site into the `site/` directory:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv run mkdocs build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The HTML output is written to `site/` (gitignored). Deploy that folder to any
|
|
81
|
+
static hosting provider.
|
|
82
|
+
|
|
83
|
+
### What is documented
|
|
84
|
+
|
|
85
|
+
The docs cover the public user-facing API:
|
|
86
|
+
|
|
87
|
+
- `bossalabs.Client` — authorize and upload video files
|
|
88
|
+
- `bossalabs.UploadClientError` — authorization failure type
|
|
89
|
+
- `bossalabs.API_BASE_URL` — default API endpoint constant
|
|
90
|
+
- `bossalabs` CLI — `process-video` command
|
|
91
|
+
|
|
92
|
+
Internal modules (`bossalabs.models`, scripts, and private helpers) are
|
|
93
|
+
excluded from the API reference.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bossalabs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
readme = "README.md"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
dependencies = [
|
|
7
|
+
"boto3",
|
|
8
|
+
"requests",
|
|
9
|
+
"fire",
|
|
10
|
+
"tomli; python_version < '3.11'",
|
|
11
|
+
"requests-toolbelt>=1.0.0",
|
|
12
|
+
"mypy",
|
|
13
|
+
"ruff",
|
|
14
|
+
"pytest",
|
|
15
|
+
"types-requests",
|
|
16
|
+
"tqdm>=4.67.3",
|
|
17
|
+
"python-dotenv>=1.2.2",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools>=68", "wheel"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
bossalabs = "bossalabs.cli.main:main"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
docs = [
|
|
29
|
+
"mkdocs>=1.6",
|
|
30
|
+
"mkdocs-material>=9.5",
|
|
31
|
+
"mkdocstrings[python]>=0.27",
|
|
32
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Public API for the BossaLabs Python client.
|
|
2
|
+
|
|
3
|
+
Import the client module to authorize and upload videos::
|
|
4
|
+
|
|
5
|
+
from bossalabs import client
|
|
6
|
+
|
|
7
|
+
c = client.Client(api_key="...", model="...")
|
|
8
|
+
result = c.upload_video("video.mp4")
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib.metadata import version
|
|
14
|
+
|
|
15
|
+
from bossalabs.client import Client
|
|
16
|
+
from bossalabs.constants import API_BASE_URL
|
|
17
|
+
from bossalabs.errors import UploadClientError
|
|
18
|
+
|
|
19
|
+
from . import client
|
|
20
|
+
|
|
21
|
+
__version__ = version("bossalabs")
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"client",
|
|
25
|
+
"Client",
|
|
26
|
+
"UploadClientError",
|
|
27
|
+
"API_BASE_URL",
|
|
28
|
+
"__version__",
|
|
29
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for bossalabs commands."""
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Command-line interface for uploading videos to BossaLabs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from bossalabs.client import Client
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _print_banner() -> None:
|
|
12
|
+
"""Print a compact CLI banner."""
|
|
13
|
+
print("=" * 54)
|
|
14
|
+
print("🎬 BossaLabs Video Processor")
|
|
15
|
+
print("=" * 54)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _print_success(result: dict[str, object]) -> None:
|
|
19
|
+
"""Print success message with formatted payload."""
|
|
20
|
+
print("✅ Video processed successfully.")
|
|
21
|
+
print(f"Model: {result.get('model')}")
|
|
22
|
+
print(
|
|
23
|
+
f"🚨 IMPORTANT: Your Job ID is: [{result.get('job_id')}] "
|
|
24
|
+
"You need this ID to request the results."
|
|
25
|
+
)
|
|
26
|
+
print(json.dumps(result, indent=2))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _print_error(message: str) -> None:
|
|
30
|
+
"""Print a formatted failure message."""
|
|
31
|
+
print(f"❌ Video processing failed: {message}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _process_video(args: argparse.Namespace) -> int:
|
|
35
|
+
"""Upload a video and print the result.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
``0`` on success, ``1`` when the upload fails.
|
|
39
|
+
"""
|
|
40
|
+
video_path = args.video_path
|
|
41
|
+
api_key = args.api_key
|
|
42
|
+
model = args.model
|
|
43
|
+
|
|
44
|
+
if not video_path:
|
|
45
|
+
raise ValueError("video_path is required")
|
|
46
|
+
if not api_key:
|
|
47
|
+
raise ValueError("api_key is required")
|
|
48
|
+
if not model:
|
|
49
|
+
raise ValueError("model is required")
|
|
50
|
+
|
|
51
|
+
_print_banner()
|
|
52
|
+
client = Client(api_key=api_key, model=model)
|
|
53
|
+
result = client.upload_video(video_path)
|
|
54
|
+
|
|
55
|
+
if result.get("success", False) is False:
|
|
56
|
+
error_message = str(result.get("error", "Some error occurred."))
|
|
57
|
+
_print_error(error_message)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
_print_success(result)
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
65
|
+
"""Build the ``bossalabs`` argument parser and subcommands."""
|
|
66
|
+
parser = argparse.ArgumentParser(prog="bossalabs")
|
|
67
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
68
|
+
|
|
69
|
+
process_video = subparsers.add_parser("process-video")
|
|
70
|
+
process_video.add_argument("video_path")
|
|
71
|
+
process_video.add_argument("model")
|
|
72
|
+
process_video.add_argument("--api_key", required=True)
|
|
73
|
+
|
|
74
|
+
return parser
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def main() -> int:
|
|
78
|
+
"""Run the ``bossalabs`` CLI.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
``0`` on success, ``1`` on upload failure, ``2`` for an unknown
|
|
82
|
+
command.
|
|
83
|
+
"""
|
|
84
|
+
parser = build_parser()
|
|
85
|
+
args = parser.parse_args()
|
|
86
|
+
|
|
87
|
+
if args.command == "process-video":
|
|
88
|
+
return _process_video(args)
|
|
89
|
+
|
|
90
|
+
parser.error(f"Unknown command: {args.command}")
|
|
91
|
+
return 2
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""BossaLabs API client for authorizing and uploading video files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, metadata, version
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import socket
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import requests # type: ignore[import-untyped]
|
|
11
|
+
from requests_toolbelt.multipart.encoder import MultipartEncoder # type: ignore[import-untyped]
|
|
12
|
+
from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor # type: ignore[import-untyped]
|
|
13
|
+
from tqdm import tqdm
|
|
14
|
+
from bossalabs.errors import UploadClientError
|
|
15
|
+
from bossalabs.models import UploadAuthorization
|
|
16
|
+
import tomllib as toml
|
|
17
|
+
from bossalabs.constants import ALLOWED_VIDEO_EXTENSIONS, API_BASE_URL
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _load_package_info() -> tuple[str, str]:
|
|
21
|
+
"""Load package name/version from metadata or pyproject fallback."""
|
|
22
|
+
distribution_name = "bossalabs"
|
|
23
|
+
try:
|
|
24
|
+
package_version = version(distribution_name)
|
|
25
|
+
package_metadata = metadata(distribution_name)
|
|
26
|
+
package_name = package_metadata.get("Name", distribution_name)
|
|
27
|
+
return package_name, package_version
|
|
28
|
+
except PackageNotFoundError:
|
|
29
|
+
pyproject_path = Path(__file__).resolve().parents[2] / "pyproject.toml"
|
|
30
|
+
pyproject_data = toml.loads(pyproject_path.read_text(encoding="utf-8"))
|
|
31
|
+
project_data = pyproject_data["project"]
|
|
32
|
+
return str(project_data["name"]), str(project_data["version"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PACKAGE_NAME, PACKAGE_VERSION = _load_package_info()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Client:
|
|
39
|
+
"""Upload videos to BossaLabs via presigned S3 POST.
|
|
40
|
+
|
|
41
|
+
Authorizes uploads through the BossaLabs API, then transfers the file with
|
|
42
|
+
``requests``. Failures from :meth:`upload_file` are returned as result
|
|
43
|
+
dictionaries rather than raised exceptions.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_key: str,
|
|
49
|
+
model: str,
|
|
50
|
+
connect_timeout: int = 30,
|
|
51
|
+
upload_timeout: int = 14400,
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Create a client for the given API key and model.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
api_key: Sent in the ``x-api-key`` header on every request.
|
|
57
|
+
model: Model name passed to the upload authorization endpoint.
|
|
58
|
+
connect_timeout: Seconds to wait for a TCP connection.
|
|
59
|
+
upload_timeout: Seconds to wait for a response body after
|
|
60
|
+
connecting (authorization and file upload).
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If ``api_key`` or ``model`` is empty.
|
|
64
|
+
"""
|
|
65
|
+
if not api_key:
|
|
66
|
+
raise ValueError("API key is required")
|
|
67
|
+
if not model:
|
|
68
|
+
raise ValueError("Model is required")
|
|
69
|
+
|
|
70
|
+
self.api_base_url = API_BASE_URL
|
|
71
|
+
self.api_key = api_key
|
|
72
|
+
self.model = model
|
|
73
|
+
self.connect_timeout = connect_timeout
|
|
74
|
+
self.upload_timeout = upload_timeout
|
|
75
|
+
self.package_version = PACKAGE_VERSION
|
|
76
|
+
self.package_name = PACKAGE_NAME
|
|
77
|
+
self.hostname = socket.gethostname()
|
|
78
|
+
self.session = requests.Session()
|
|
79
|
+
self.session.headers.update(
|
|
80
|
+
{
|
|
81
|
+
"x-api-key": self.api_key,
|
|
82
|
+
"Accept": "application/json",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
"User-Agent": f"{self.package_name}/{self.package_version} ({self.hostname})",
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def _authorize_upload(self) -> UploadAuthorization | UploadClientError:
|
|
89
|
+
url = f"{self.api_base_url}/upload-videos"
|
|
90
|
+
response = self.session.post(
|
|
91
|
+
url,
|
|
92
|
+
json={"model": self.model},
|
|
93
|
+
timeout=(self.connect_timeout, self.upload_timeout),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if response.status_code != 200:
|
|
97
|
+
return UploadClientError(
|
|
98
|
+
message="Upload authorization failed.",
|
|
99
|
+
status_code=response.status_code,
|
|
100
|
+
body=response.text,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
payload = response.json()
|
|
104
|
+
|
|
105
|
+
return UploadAuthorization(
|
|
106
|
+
job_id=payload["job_id"],
|
|
107
|
+
model=payload["model"],
|
|
108
|
+
bucket=payload["bucket"],
|
|
109
|
+
key=payload["key"],
|
|
110
|
+
expires_in_seconds=int(payload["expires_in_seconds"]),
|
|
111
|
+
max_upload_size_bytes=int(payload["max_upload_size_bytes"]),
|
|
112
|
+
max_upload_size_gb=float(payload["max_upload_size_gb"]),
|
|
113
|
+
upload_url=payload["upload"]["url"],
|
|
114
|
+
upload_fields=dict(payload["upload"]["fields"]),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def upload_video(
|
|
118
|
+
self,
|
|
119
|
+
video_path: str | Path,
|
|
120
|
+
content_type: str = "video/mp4",
|
|
121
|
+
) -> dict[str, Any]:
|
|
122
|
+
"""Upload a local video file to S3 using boto3.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
video_path: Local path to the video file that will be uploaded.
|
|
126
|
+
content_type: MIME type used for uploaded content.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A result dictionary. On success, ``success`` is ``True`` and the
|
|
130
|
+
dict includes ``job_id``, ``model``, ``file_size_bytes``, and
|
|
131
|
+
``status_code``. On failure, ``success`` is ``False`` and
|
|
132
|
+
``error`` contains a human-readable message; other fields may be
|
|
133
|
+
``None``.
|
|
134
|
+
"""
|
|
135
|
+
path = Path(video_path)
|
|
136
|
+
|
|
137
|
+
if path.suffix.lower() not in ALLOWED_VIDEO_EXTENSIONS:
|
|
138
|
+
return {
|
|
139
|
+
"job_id": None,
|
|
140
|
+
"model": None,
|
|
141
|
+
"file_size_bytes": None,
|
|
142
|
+
"status_code": None,
|
|
143
|
+
"success": False,
|
|
144
|
+
"error": f"File extension not allowed: {path.suffix}",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if not path.exists():
|
|
148
|
+
return {
|
|
149
|
+
"job_id": None,
|
|
150
|
+
"model": None,
|
|
151
|
+
"file_size_bytes": None,
|
|
152
|
+
"status_code": None,
|
|
153
|
+
"success": False,
|
|
154
|
+
"error": f"File not found: {path}",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
return {
|
|
159
|
+
"job_id": None,
|
|
160
|
+
"model": None,
|
|
161
|
+
"file_size_bytes": None,
|
|
162
|
+
"status_code": None,
|
|
163
|
+
"success": False,
|
|
164
|
+
"error": f"Path is not a file: {path}",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
auth = self._authorize_upload()
|
|
168
|
+
if isinstance(auth, UploadClientError):
|
|
169
|
+
return {
|
|
170
|
+
"job_id": None,
|
|
171
|
+
"model": None,
|
|
172
|
+
"file_size_bytes": None,
|
|
173
|
+
"status_code": None,
|
|
174
|
+
"success": False,
|
|
175
|
+
"error": auth.message,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
file_size = path.stat().st_size
|
|
179
|
+
if file_size > auth.max_upload_size_bytes:
|
|
180
|
+
return {
|
|
181
|
+
"job_id": None,
|
|
182
|
+
"model": None,
|
|
183
|
+
"file_size_bytes": file_size,
|
|
184
|
+
"status_code": None,
|
|
185
|
+
"success": False,
|
|
186
|
+
"error": f"File too large. size={file_size} bytes, max={auth.max_upload_size_bytes} bytes",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
final_content_type = content_type or "application/octet-stream"
|
|
190
|
+
|
|
191
|
+
fields = dict(auth.upload_fields)
|
|
192
|
+
|
|
193
|
+
if "Content-Type" not in fields:
|
|
194
|
+
fields["Content-Type"] = final_content_type
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
with path.open("rb") as f:
|
|
198
|
+
multipart_fields = {
|
|
199
|
+
**fields,
|
|
200
|
+
"file": (path.name, f, final_content_type),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
encoder = MultipartEncoder(fields=multipart_fields)
|
|
204
|
+
|
|
205
|
+
with tqdm(
|
|
206
|
+
total=encoder.len,
|
|
207
|
+
unit="B",
|
|
208
|
+
unit_scale=True,
|
|
209
|
+
unit_divisor=1024,
|
|
210
|
+
desc=f"Uploading {path.name}",
|
|
211
|
+
) as progress_bar:
|
|
212
|
+
last_bytes_read = 0
|
|
213
|
+
|
|
214
|
+
def callback(monitor: MultipartEncoderMonitor) -> None:
|
|
215
|
+
nonlocal last_bytes_read
|
|
216
|
+
progress_bar.update(monitor.bytes_read - last_bytes_read)
|
|
217
|
+
last_bytes_read = monitor.bytes_read
|
|
218
|
+
|
|
219
|
+
monitor = MultipartEncoderMonitor(encoder, callback)
|
|
220
|
+
|
|
221
|
+
response = requests.post(
|
|
222
|
+
auth.upload_url,
|
|
223
|
+
data=monitor,
|
|
224
|
+
headers={"Content-Type": monitor.content_type},
|
|
225
|
+
timeout=(self.connect_timeout, self.upload_timeout),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if response.status_code not in (200, 201, 204):
|
|
229
|
+
return {
|
|
230
|
+
"job_id": None,
|
|
231
|
+
"model": None,
|
|
232
|
+
"file_size_bytes": file_size,
|
|
233
|
+
"status_code": response.status_code,
|
|
234
|
+
"success": False,
|
|
235
|
+
"error": response.text,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
except requests.RequestException as error:
|
|
239
|
+
return {
|
|
240
|
+
"job_id": None,
|
|
241
|
+
"model": None,
|
|
242
|
+
"file_size_bytes": file_size,
|
|
243
|
+
"status_code": None,
|
|
244
|
+
"success": False,
|
|
245
|
+
"error": str(error),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
"job_id": auth.job_id,
|
|
250
|
+
"model": auth.model,
|
|
251
|
+
"file_size_bytes": file_size,
|
|
252
|
+
"status_code": response.status_code,
|
|
253
|
+
"success": True,
|
|
254
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Base URL for the BossaLabs API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
|
|
8
|
+
ENV = os.getenv("ENV", "DEV")
|
|
9
|
+
|
|
10
|
+
if ENV == "DEV":
|
|
11
|
+
API_BASE_URL = "https://3q6tjso3ce.execute-api.us-east-1.amazonaws.com/DEV"
|
|
12
|
+
elif ENV == "PROD":
|
|
13
|
+
API_BASE_URL = "https://3q6tjso3ce.execute-api.us-east-1.amazonaws.com/"
|
|
14
|
+
else:
|
|
15
|
+
raise ValueError(f"Invalid environment: {ENV}")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ALLOWED_VIDEO_EXTENSIONS = [".mp4", ".mov", ".avi", ".mkv", ".webm"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Error types used by the BossaLabs client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UploadClientError(Exception):
|
|
7
|
+
"""Error returned when upload authorization fails.
|
|
8
|
+
|
|
9
|
+
:class:`~bossalabs.client.Client` returns this from internal
|
|
10
|
+
authorization calls. :meth:`~bossalabs.client.Client.upload_file`
|
|
11
|
+
converts it into a failure result dictionary instead of raising.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
message: str,
|
|
17
|
+
*,
|
|
18
|
+
status_code: int | None = None,
|
|
19
|
+
body: str | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.message = message
|
|
22
|
+
self.status_code = status_code
|
|
23
|
+
self.body = body
|
|
24
|
+
super().__init__(message)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Data models for BossaLabs API responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class AwsCredentials:
|
|
10
|
+
"""Temporary AWS credentials for direct S3 access.
|
|
11
|
+
|
|
12
|
+
Not used by the current upload flow, which relies on presigned POST URLs.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
access_key_id: str
|
|
16
|
+
secret_access_key: str
|
|
17
|
+
session_token: str | None = None
|
|
18
|
+
region: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class UploadAuthorization:
|
|
23
|
+
"""Presigned upload details returned by the BossaLabs API."""
|
|
24
|
+
|
|
25
|
+
job_id: str
|
|
26
|
+
model: str
|
|
27
|
+
bucket: str
|
|
28
|
+
key: str
|
|
29
|
+
expires_in_seconds: int
|
|
30
|
+
max_upload_size_bytes: int
|
|
31
|
+
max_upload_size_gb: float
|
|
32
|
+
upload_url: str
|
|
33
|
+
upload_fields: dict[str, str]
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Fire-based script entry point for uploading a single video file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from bossalabs.client import Client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(
|
|
9
|
+
api_key: str,
|
|
10
|
+
upload_video_path: str,
|
|
11
|
+
model: str,
|
|
12
|
+
content_type: str = "video/mp4",
|
|
13
|
+
) -> dict[str, object]:
|
|
14
|
+
"""Upload a video file via :class:`~bossalabs.client.Client`.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
api_key: API key used for authorization requests.
|
|
18
|
+
upload_video_path: Local path of the file to upload.
|
|
19
|
+
model: Model name to use for processing the video.
|
|
20
|
+
content_type: MIME type sent with the uploaded file.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Upload result payload returned by `Client.upload_video`.
|
|
24
|
+
"""
|
|
25
|
+
client = Client(api_key=api_key, model=model)
|
|
26
|
+
return client.upload_video(upload_video_path, content_type=content_type)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
import fire
|
|
31
|
+
|
|
32
|
+
fire.Fire(main)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bossalabs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Requires-Python: >=3.10
|
|
5
|
+
Description-Content-Type: text/markdown
|
|
6
|
+
Requires-Dist: boto3
|
|
7
|
+
Requires-Dist: requests
|
|
8
|
+
Requires-Dist: fire
|
|
9
|
+
Requires-Dist: tomli; python_version < "3.11"
|
|
10
|
+
Requires-Dist: requests-toolbelt>=1.0.0
|
|
11
|
+
Requires-Dist: mypy
|
|
12
|
+
Requires-Dist: ruff
|
|
13
|
+
Requires-Dist: pytest
|
|
14
|
+
Requires-Dist: types-requests
|
|
15
|
+
Requires-Dist: tqdm>=4.67.3
|
|
16
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/)
|
|
19
|
+
[](https://github.com/computervisionsports/bossa-client/actions/workflows/ci.yml)
|
|
20
|
+
|
|
21
|
+
# BossaLabs Python Client
|
|
22
|
+
|
|
23
|
+
Small Python client for requesting upload authorization and uploading
|
|
24
|
+
video files using a presigned S3 POST.
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv venv .venv
|
|
30
|
+
source .venv/bin/activate
|
|
31
|
+
uv pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or with plain pip:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or from PyPI:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install bossalabs
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Python usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from bossalabs import Client
|
|
50
|
+
|
|
51
|
+
client = Client(
|
|
52
|
+
api_key="YOUR_API_KEY",
|
|
53
|
+
model="MODEL_NAME"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = client.upload_video("/path/to/video.mp4")
|
|
57
|
+
print(result)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## CLI usage
|
|
61
|
+
|
|
62
|
+
After installation, you can run:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bossalabs process-video "/path/to/video.mp4" "MODEL_NAME" --api_key "YOUR_API_KEY"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Documentation
|
|
69
|
+
|
|
70
|
+
Full documentation is built with [MkDocs](https://www.mkdocs.org/) and
|
|
71
|
+
[Material for MkDocs](https://squidfunk.github.io/mkdocs-material/).
|
|
72
|
+
|
|
73
|
+
### Install documentation dependencies
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
uv sync --group docs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Serve docs locally
|
|
80
|
+
|
|
81
|
+
Preview the site with live reload:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
uv run mkdocs serve
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
|
|
88
|
+
|
|
89
|
+
### Build static HTML
|
|
90
|
+
|
|
91
|
+
Generate the static site into the `site/` directory:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv run mkdocs build
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The HTML output is written to `site/` (gitignored). Deploy that folder to any
|
|
98
|
+
static hosting provider.
|
|
99
|
+
|
|
100
|
+
### What is documented
|
|
101
|
+
|
|
102
|
+
The docs cover the public user-facing API:
|
|
103
|
+
|
|
104
|
+
- `bossalabs.Client` — authorize and upload video files
|
|
105
|
+
- `bossalabs.UploadClientError` — authorization failure type
|
|
106
|
+
- `bossalabs.API_BASE_URL` — default API endpoint constant
|
|
107
|
+
- `bossalabs` CLI — `process-video` command
|
|
108
|
+
|
|
109
|
+
Internal modules (`bossalabs.models`, scripts, and private helpers) are
|
|
110
|
+
excluded from the API reference.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/bossalabs/__init__.py
|
|
4
|
+
src/bossalabs/client.py
|
|
5
|
+
src/bossalabs/constants.py
|
|
6
|
+
src/bossalabs/errors.py
|
|
7
|
+
src/bossalabs/models.py
|
|
8
|
+
src/bossalabs.egg-info/PKG-INFO
|
|
9
|
+
src/bossalabs.egg-info/SOURCES.txt
|
|
10
|
+
src/bossalabs.egg-info/dependency_links.txt
|
|
11
|
+
src/bossalabs.egg-info/entry_points.txt
|
|
12
|
+
src/bossalabs.egg-info/requires.txt
|
|
13
|
+
src/bossalabs.egg-info/top_level.txt
|
|
14
|
+
src/bossalabs/cli/__init__.py
|
|
15
|
+
src/bossalabs/cli/main.py
|
|
16
|
+
src/bossalabs/scripts/__init__.py
|
|
17
|
+
src/bossalabs/scripts/upload_video.py
|
|
18
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
bossalabs
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from bossalabs import client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DummySession:
|
|
7
|
+
"""Minimal requests.Session test double."""
|
|
8
|
+
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
self.headers: dict[str, str] = {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_create_client_object_with_mocked_session(monkeypatch) -> None:
|
|
14
|
+
"""Creates a client object without real network dependencies."""
|
|
15
|
+
|
|
16
|
+
monkeypatch.setattr("bossalabs.client.requests.Session", DummySession)
|
|
17
|
+
|
|
18
|
+
c = client.Client(api_key="test-api-key", model="my-dummy-model")
|
|
19
|
+
|
|
20
|
+
assert c.api_key == "test-api-key"
|
|
21
|
+
assert c.model == "my-dummy-model"
|
|
22
|
+
assert c.session.headers["x-api-key"] == "test-api-key"
|
|
23
|
+
assert c.session.headers["Accept"] == "application/json"
|