serva 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.
- serva-0.1.0/LICENSE +5 -0
- serva-0.1.0/PKG-INFO +59 -0
- serva-0.1.0/README.md +44 -0
- serva-0.1.0/pyproject.toml +29 -0
- serva-0.1.0/setup.cfg +4 -0
- serva-0.1.0/src/serva/__init__.py +23 -0
- serva-0.1.0/src/serva/_decode.py +45 -0
- serva-0.1.0/src/serva/_encode.py +46 -0
- serva-0.1.0/src/serva/_http.py +79 -0
- serva-0.1.0/src/serva/client.py +45 -0
- serva-0.1.0/src/serva/exceptions.py +39 -0
- serva-0.1.0/src/serva/models.py +22 -0
- serva-0.1.0/src/serva/py.typed +0 -0
- serva-0.1.0/src/serva.egg-info/PKG-INFO +59 -0
- serva-0.1.0/src/serva.egg-info/SOURCES.txt +17 -0
- serva-0.1.0/src/serva.egg-info/dependency_links.txt +1 -0
- serva-0.1.0/src/serva.egg-info/requires.txt +4 -0
- serva-0.1.0/src/serva.egg-info/top_level.txt +1 -0
- serva-0.1.0/tests/test_client.py +20 -0
serva-0.1.0/LICENSE
ADDED
serva-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serva
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Serva encode/decode API
|
|
5
|
+
Author: Servamind
|
|
6
|
+
License: Proprietary - All Rights Reserved
|
|
7
|
+
Project-URL: Homepage, https://serva.servamind.com
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# serva
|
|
17
|
+
|
|
18
|
+
Official Python client for the Serva encode/decode API.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install serva
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Generate an API key from [serva.servamind.com](https://serva.servamind.com), then:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from serva import Serva
|
|
32
|
+
|
|
33
|
+
client = Serva(api_key="sk_live_...") # or set SERVA_API_KEY
|
|
34
|
+
|
|
35
|
+
# Encode a file to the .serva format
|
|
36
|
+
result = client.encode("photo.raw", password="my-secret")
|
|
37
|
+
print(result.output_path, result.savings_percent)
|
|
38
|
+
|
|
39
|
+
# Decode it back
|
|
40
|
+
client.decode("photo.serva", password="my-secret", output="photo.raw")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Setting | How | Default |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| API key | `Serva(api_key=...)` or `SERVA_API_KEY` | — (required) |
|
|
48
|
+
| Base URL | `Serva(base_url=...)` or `SERVA_BASE_URL` | `https://serva.servamind.com` |
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Errors
|
|
53
|
+
|
|
54
|
+
All failures raise a subclass of `ServaError`: `AuthError` (401),
|
|
55
|
+
`PaymentRequiredError` (403), `FileTooLargeError` (413), `RateLimitError` (429).
|
|
56
|
+
|
|
57
|
+
## Versioning
|
|
58
|
+
|
|
59
|
+
Semantic versioning. The current version is available as `serva.__version__`.
|
serva-0.1.0/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# serva
|
|
2
|
+
|
|
3
|
+
Official Python client for the Serva encode/decode API.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install serva
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
Generate an API key from [serva.servamind.com](https://serva.servamind.com), then:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from serva import Serva
|
|
17
|
+
|
|
18
|
+
client = Serva(api_key="sk_live_...") # or set SERVA_API_KEY
|
|
19
|
+
|
|
20
|
+
# Encode a file to the .serva format
|
|
21
|
+
result = client.encode("photo.raw", password="my-secret")
|
|
22
|
+
print(result.output_path, result.savings_percent)
|
|
23
|
+
|
|
24
|
+
# Decode it back
|
|
25
|
+
client.decode("photo.serva", password="my-secret", output="photo.raw")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
| Setting | How | Default |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| API key | `Serva(api_key=...)` or `SERVA_API_KEY` | — (required) |
|
|
33
|
+
| Base URL | `Serva(base_url=...)` or `SERVA_BASE_URL` | `https://serva.servamind.com` |
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
## Errors
|
|
38
|
+
|
|
39
|
+
All failures raise a subclass of `ServaError`: `AuthError` (401),
|
|
40
|
+
`PaymentRequiredError` (403), `FileTooLargeError` (413), `RateLimitError` (429).
|
|
41
|
+
|
|
42
|
+
## Versioning
|
|
43
|
+
|
|
44
|
+
Semantic versioning. The current version is available as `serva.__version__`.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "serva"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the Serva encode/decode API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary - All Rights Reserved" }
|
|
12
|
+
authors = [{ name = "Servamind" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.27",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=7.4",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://serva.servamind.com"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.package-data]
|
|
29
|
+
serva = ["py.typed"]
|
serva-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from serva.client import Serva
|
|
2
|
+
from serva.models import DecodeResult, EncodeResult
|
|
3
|
+
from serva.exceptions import (
|
|
4
|
+
AuthError,
|
|
5
|
+
FileTooLargeError,
|
|
6
|
+
PaymentRequiredError,
|
|
7
|
+
RateLimitError,
|
|
8
|
+
ServaError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Serva",
|
|
15
|
+
"EncodeResult",
|
|
16
|
+
"DecodeResult",
|
|
17
|
+
"ServaError",
|
|
18
|
+
"AuthError",
|
|
19
|
+
"PaymentRequiredError",
|
|
20
|
+
"FileTooLargeError",
|
|
21
|
+
"RateLimitError",
|
|
22
|
+
"__version__",
|
|
23
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from serva._http import HttpClient
|
|
7
|
+
from serva.models import DecodeResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def decode_file(
|
|
11
|
+
http: HttpClient, path: str, password: str, output: str | None = None
|
|
12
|
+
) -> DecodeResult:
|
|
13
|
+
"""Decode a .serva file back to the original. Returns where it was written."""
|
|
14
|
+
source = Path(path)
|
|
15
|
+
size = source.stat().st_size
|
|
16
|
+
file_reference = str(uuid.uuid4())
|
|
17
|
+
|
|
18
|
+
init = http.post_json(
|
|
19
|
+
"/api/decode",
|
|
20
|
+
{
|
|
21
|
+
"file_reference": file_reference,
|
|
22
|
+
"file_size_bytes": size,
|
|
23
|
+
"user_password": password,
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
result = http.post_bytes(
|
|
28
|
+
"/api/decode",
|
|
29
|
+
source,
|
|
30
|
+
{
|
|
31
|
+
"X-Streaming-Token": init["streaming_token"],
|
|
32
|
+
"X-Decode-Password": password,
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
original_filename = result.get("original_filename", "decoded.out")
|
|
37
|
+
destination = Path(output) if output else source.parent / original_filename
|
|
38
|
+
http.download_to(f"/download/{result['file_id']}", destination)
|
|
39
|
+
|
|
40
|
+
return DecodeResult(
|
|
41
|
+
output_path=destination,
|
|
42
|
+
file_id=result["file_id"],
|
|
43
|
+
original_filename=original_filename,
|
|
44
|
+
size_bytes=result.get("file_size_bytes", 0),
|
|
45
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from serva._http import HttpClient
|
|
7
|
+
from serva.models import EncodeResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def encode_file(
|
|
11
|
+
http: HttpClient, path: str, password: str, output: str | None = None
|
|
12
|
+
) -> EncodeResult:
|
|
13
|
+
"""Encode a file to the .serva format. Returns where it was written."""
|
|
14
|
+
source = Path(path)
|
|
15
|
+
size = source.stat().st_size
|
|
16
|
+
file_reference = str(uuid.uuid4())
|
|
17
|
+
|
|
18
|
+
init = http.post_json(
|
|
19
|
+
"/api/encode",
|
|
20
|
+
{
|
|
21
|
+
"file_reference": file_reference,
|
|
22
|
+
"idempotency_key": str(uuid.uuid4()),
|
|
23
|
+
"file_size_bytes": size,
|
|
24
|
+
"file_extension": source.suffix[:10],
|
|
25
|
+
"original_filename": source.name[:512],
|
|
26
|
+
"user_password": password,
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
stats = http.post_bytes(
|
|
31
|
+
f"/api/stream/{file_reference}",
|
|
32
|
+
source,
|
|
33
|
+
{"X-Streaming-Token": init["streaming_token"]},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
destination = Path(output) if output else source.with_suffix(".serva")
|
|
37
|
+
http.download_to(f"/download/{init['file_id']}", destination)
|
|
38
|
+
|
|
39
|
+
return EncodeResult(
|
|
40
|
+
output_path=destination,
|
|
41
|
+
file_id=init["file_id"],
|
|
42
|
+
original_size_bytes=stats.get("original_size_bytes", size),
|
|
43
|
+
encoded_size_bytes=stats.get("encoded_size_bytes", 0),
|
|
44
|
+
savings_percent=stats.get("savings_percent", 0.0),
|
|
45
|
+
roundtrip_ok=stats.get("roundtrip_hashes_match", False),
|
|
46
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Iterator, Mapping
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from serva.exceptions import AuthError, error_from_response
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://serva.servamind.com"
|
|
12
|
+
UPLOAD_CHUNK_SIZE = 1024 * 1024
|
|
13
|
+
DOWNLOAD_CHUNK_SIZE = 1024 * 1024
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HttpClient:
|
|
17
|
+
"""Thin wrapper over httpx that carries auth and maps errors."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
base_url: str | None = None,
|
|
23
|
+
timeout: float | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
api_key = api_key or os.environ.get("SERVA_API_KEY")
|
|
26
|
+
if not api_key:
|
|
27
|
+
raise AuthError("No API key provided. Pass api_key=... or set SERVA_API_KEY.")
|
|
28
|
+
base = base_url or os.environ.get("SERVA_BASE_URL") or DEFAULT_BASE_URL
|
|
29
|
+
self._client = httpx.Client(
|
|
30
|
+
base_url=base.rstrip("/"),
|
|
31
|
+
timeout=timeout,
|
|
32
|
+
headers={"Authorization": f"Bearer {api_key}"},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def post_json(self, path: str, payload: Mapping[str, Any]) -> dict:
|
|
36
|
+
resp = self._client.post(path, json=payload)
|
|
37
|
+
return self._json_or_raise(resp)
|
|
38
|
+
|
|
39
|
+
def post_bytes(
|
|
40
|
+
self, path: str, source: Path, headers: Mapping[str, str]
|
|
41
|
+
) -> dict:
|
|
42
|
+
merged = {"Content-Type": "application/octet-stream", **headers}
|
|
43
|
+
resp = self._client.post(path, content=_file_chunks(source), headers=merged)
|
|
44
|
+
return self._json_or_raise(resp)
|
|
45
|
+
|
|
46
|
+
def download_to(self, path: str, destination: Path) -> None:
|
|
47
|
+
with self._client.stream("GET", path) as resp:
|
|
48
|
+
if resp.status_code >= 400:
|
|
49
|
+
resp.read()
|
|
50
|
+
self._raise(resp)
|
|
51
|
+
with open(destination, "wb") as out:
|
|
52
|
+
for chunk in resp.iter_bytes(DOWNLOAD_CHUNK_SIZE):
|
|
53
|
+
out.write(chunk)
|
|
54
|
+
|
|
55
|
+
def close(self) -> None:
|
|
56
|
+
self._client.close()
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
def _json_or_raise(resp: httpx.Response) -> dict:
|
|
60
|
+
if resp.status_code >= 400:
|
|
61
|
+
HttpClient._raise(resp)
|
|
62
|
+
return resp.json()
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _raise(resp: httpx.Response) -> None:
|
|
66
|
+
try:
|
|
67
|
+
detail = resp.json()
|
|
68
|
+
except Exception:
|
|
69
|
+
detail = resp.text
|
|
70
|
+
raise error_from_response(resp.status_code, detail)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _file_chunks(source: Path) -> Iterator[bytes]:
|
|
74
|
+
with open(source, "rb") as f:
|
|
75
|
+
while True:
|
|
76
|
+
chunk = f.read(UPLOAD_CHUNK_SIZE)
|
|
77
|
+
if not chunk:
|
|
78
|
+
break
|
|
79
|
+
yield chunk
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from serva._decode import decode_file
|
|
4
|
+
from serva._encode import encode_file
|
|
5
|
+
from serva._http import HttpClient
|
|
6
|
+
from serva.models import DecodeResult, EncodeResult
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Serva:
|
|
10
|
+
"""Client for the Serva encode/decode API.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
client = Serva(api_key="sk_live_...")
|
|
14
|
+
client.encode("photo.raw", password="secret")
|
|
15
|
+
client.decode("photo.serva", password="secret")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
api_key: str | None = None,
|
|
21
|
+
base_url: str | None = None,
|
|
22
|
+
timeout: float | None = None,
|
|
23
|
+
) -> None:
|
|
24
|
+
self._http = HttpClient(api_key=api_key, base_url=base_url, timeout=timeout)
|
|
25
|
+
|
|
26
|
+
def encode(
|
|
27
|
+
self, path: str, password: str, output: str | None = None
|
|
28
|
+
) -> EncodeResult:
|
|
29
|
+
"""Encode a file to the .serva format. Returns where it was written."""
|
|
30
|
+
return encode_file(self._http, path, password, output)
|
|
31
|
+
|
|
32
|
+
def decode(
|
|
33
|
+
self, path: str, password: str, output: str | None = None
|
|
34
|
+
) -> DecodeResult:
|
|
35
|
+
"""Decode a .serva file back to the original. Returns where it was written."""
|
|
36
|
+
return decode_file(self._http, path, password, output)
|
|
37
|
+
|
|
38
|
+
def close(self) -> None:
|
|
39
|
+
self._http.close()
|
|
40
|
+
|
|
41
|
+
def __enter__(self) -> "Serva":
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def __exit__(self, *exc: object) -> None:
|
|
45
|
+
self.close()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ServaError(Exception):
|
|
5
|
+
"""Base error for all Serva client failures."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.status_code = status_code
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AuthError(ServaError):
|
|
13
|
+
"""The API key is missing, invalid, or expired (HTTP 401)."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PaymentRequiredError(ServaError):
|
|
17
|
+
"""A payment method is required to continue (HTTP 403)."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FileTooLargeError(ServaError):
|
|
21
|
+
"""The input file exceeds the maximum allowed size (HTTP 413)."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RateLimitError(ServaError):
|
|
25
|
+
"""The request was rate limited (HTTP 429)."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def error_from_response(status_code: int, detail: object) -> ServaError:
|
|
29
|
+
"""Map an HTTP status code to the matching typed error."""
|
|
30
|
+
message = f"Serva API error {status_code}: {detail}"
|
|
31
|
+
if status_code == 401:
|
|
32
|
+
return AuthError(message, status_code)
|
|
33
|
+
if status_code == 403:
|
|
34
|
+
return PaymentRequiredError(message, status_code)
|
|
35
|
+
if status_code == 413:
|
|
36
|
+
return FileTooLargeError(message, status_code)
|
|
37
|
+
if status_code == 429:
|
|
38
|
+
return RateLimitError(message, status_code)
|
|
39
|
+
return ServaError(message, status_code)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class EncodeResult:
|
|
9
|
+
output_path: Path
|
|
10
|
+
file_id: str
|
|
11
|
+
original_size_bytes: int
|
|
12
|
+
encoded_size_bytes: int
|
|
13
|
+
savings_percent: float
|
|
14
|
+
roundtrip_ok: bool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DecodeResult:
|
|
19
|
+
output_path: Path
|
|
20
|
+
file_id: str
|
|
21
|
+
original_filename: str
|
|
22
|
+
size_bytes: int
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: serva
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Serva encode/decode API
|
|
5
|
+
Author: Servamind
|
|
6
|
+
License: Proprietary - All Rights Reserved
|
|
7
|
+
Project-URL: Homepage, https://serva.servamind.com
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# serva
|
|
17
|
+
|
|
18
|
+
Official Python client for the Serva encode/decode API.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install serva
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Generate an API key from [serva.servamind.com](https://serva.servamind.com), then:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from serva import Serva
|
|
32
|
+
|
|
33
|
+
client = Serva(api_key="sk_live_...") # or set SERVA_API_KEY
|
|
34
|
+
|
|
35
|
+
# Encode a file to the .serva format
|
|
36
|
+
result = client.encode("photo.raw", password="my-secret")
|
|
37
|
+
print(result.output_path, result.savings_percent)
|
|
38
|
+
|
|
39
|
+
# Decode it back
|
|
40
|
+
client.decode("photo.serva", password="my-secret", output="photo.raw")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Setting | How | Default |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| API key | `Serva(api_key=...)` or `SERVA_API_KEY` | — (required) |
|
|
48
|
+
| Base URL | `Serva(base_url=...)` or `SERVA_BASE_URL` | `https://serva.servamind.com` |
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
## Errors
|
|
53
|
+
|
|
54
|
+
All failures raise a subclass of `ServaError`: `AuthError` (401),
|
|
55
|
+
`PaymentRequiredError` (403), `FileTooLargeError` (413), `RateLimitError` (429).
|
|
56
|
+
|
|
57
|
+
## Versioning
|
|
58
|
+
|
|
59
|
+
Semantic versioning. The current version is available as `serva.__version__`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/serva/__init__.py
|
|
5
|
+
src/serva/_decode.py
|
|
6
|
+
src/serva/_encode.py
|
|
7
|
+
src/serva/_http.py
|
|
8
|
+
src/serva/client.py
|
|
9
|
+
src/serva/exceptions.py
|
|
10
|
+
src/serva/models.py
|
|
11
|
+
src/serva/py.typed
|
|
12
|
+
src/serva.egg-info/PKG-INFO
|
|
13
|
+
src/serva.egg-info/SOURCES.txt
|
|
14
|
+
src/serva.egg-info/dependency_links.txt
|
|
15
|
+
src/serva.egg-info/requires.txt
|
|
16
|
+
src/serva.egg-info/top_level.txt
|
|
17
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
serva
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import serva
|
|
4
|
+
from serva import AuthError, Serva
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_version():
|
|
8
|
+
assert serva.__version__ == "0.1.0"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_requires_api_key(monkeypatch):
|
|
12
|
+
monkeypatch.delenv("SERVA_API_KEY", raising=False)
|
|
13
|
+
with pytest.raises(AuthError):
|
|
14
|
+
Serva(api_key=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_reads_key_from_env(monkeypatch):
|
|
18
|
+
monkeypatch.setenv("SERVA_API_KEY", "sk_live_test")
|
|
19
|
+
client = Serva(base_url="http://localhost")
|
|
20
|
+
client.close()
|