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 ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (c) Servamind. All Rights Reserved.
2
+
3
+ This software and associated files are proprietary to Servamind. Unauthorized
4
+ copying, modification, distribution, or use of this software, via any medium,
5
+ is strictly prohibited without prior written permission.
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,4 @@
1
+ httpx>=0.27
2
+
3
+ [dev]
4
+ pytest>=7.4
@@ -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()