aurastamp-sdk 1.0.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,13 @@
1
+ node_modules
2
+ .DS_store
3
+
4
+ .venv
5
+ __pycache__/
6
+ .ruff_cache/
7
+ .pytest_cache/
8
+ dist/
9
+ .vercel
10
+ .firebase/
11
+ firebase-debug.log
12
+ firestore-debug.log
13
+ /package-lock.json
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.5
2
+ Name: aurastamp-sdk
3
+ Version: 1.0.0
4
+ Summary: Hide and reveal short messages inside images, via the Aurastamp API
5
+ Project-URL: Homepage, https://aurastamp.com
6
+ Project-URL: Repository, https://github.com/sunutf/aurastamp
7
+ Author: sunutf
8
+ License: MIT
9
+ Keywords: aurastamp,image,steganography,watermark
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Multimedia :: Graphics
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: requests>=2.31
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: responses>=0.25; extra == 'dev'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Aurastamp SDK (Python)
21
+
22
+ Hide and reveal short messages inside images.
23
+
24
+ ```sh
25
+ pip install aurastamp-sdk
26
+ ```
27
+
28
+ ## The payload is 7 bytes
29
+
30
+ The message is embedded in the image itself, and the model reserves exactly
31
+ **7 bytes** for it. Anything longer is rejected before a request is made.
32
+
33
+ That is 7 characters of ASCII, but fewer for anything else — `'가나'` is two
34
+ characters and six bytes.
35
+
36
+ To hide something longer, store the text somewhere and embed a short key
37
+ instead; that is what [aurastamp.com](https://aurastamp.com) does.
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from aurastamp_sdk import Aurastamp
43
+
44
+ aurastamp = Aurastamp() # or Aurastamp("http://localhost:8000")
45
+
46
+ # Hide a message
47
+ encoded = aurastamp.image.encode_file(
48
+ image_file_path="photo.png",
49
+ message="hello",
50
+ output_path="encoded.png",
51
+ )
52
+
53
+ # Reveal it again
54
+ print(aurastamp.image.decode_file(image_file_path="encoded.png"))
55
+ #=> ' hello' (padded to 7 bytes)
56
+ ```
57
+
58
+ Working with bytes directly:
59
+
60
+ ```python
61
+ with open("photo.png", "rb") as f:
62
+ encoded = aurastamp.image.encode(image=f.read(), message="hello", filename="photo.png")
63
+
64
+ message = aurastamp.image.decode(image=encoded, filename="encoded.png")
65
+ ```
66
+
67
+ ## Configuration
68
+
69
+ | Setting | Default |
70
+ | ------------------ | --------------------------- |
71
+ | `server_url` arg | `AURASTAMP_API_URL` env var |
72
+ | `AURASTAMP_API_URL`| `https://api.aurastamp.com` |
73
+
74
+ ## Errors
75
+
76
+ Every failure raises `AurastampError`, carrying the server's `detail` message
77
+ and the HTTP status where there is one.
78
+
79
+ ```python
80
+ from aurastamp_sdk import AurastampError
81
+
82
+ try:
83
+ aurastamp.image.encode_file(image_file_path="photo.png", message="way too long")
84
+ except AurastampError as error:
85
+ print(error, error.status)
86
+ ```
87
+
88
+ ## Coming from the `aurastamp` package
89
+
90
+ This supersedes `aurastamp` on PyPI, which is unmaintained and published from an
91
+ account this project no longer controls. Its last release could not talk to the
92
+ current API: it base64-decoded a response that was already raw PNG bytes and
93
+ read keys off the decode response that the server never sends.
94
+
95
+ The import is `aurastamp_sdk`, not `aurastamp` — two distributions installing
96
+ the same top-level package would overwrite each other's files.
97
+
98
+ If you depend on `aurastamp`, switch to `aurastamp-sdk`. What changed:
99
+
100
+ - `encode` / `encode_local_file` → `encode` / `encode_file`, and they now return
101
+ **PNG bytes** rather than a `requests.Response`
102
+ - `decode` / `decode_local_file` → `decode` / `decode_file`, returning the
103
+ message **string**
104
+ - the unused `model`, `return_type` and `hidden_image` arguments are gone
105
+ - failures raise `AurastampError` instead of returning a non-2xx response
@@ -0,0 +1,86 @@
1
+ # Aurastamp SDK (Python)
2
+
3
+ Hide and reveal short messages inside images.
4
+
5
+ ```sh
6
+ pip install aurastamp-sdk
7
+ ```
8
+
9
+ ## The payload is 7 bytes
10
+
11
+ The message is embedded in the image itself, and the model reserves exactly
12
+ **7 bytes** for it. Anything longer is rejected before a request is made.
13
+
14
+ That is 7 characters of ASCII, but fewer for anything else — `'가나'` is two
15
+ characters and six bytes.
16
+
17
+ To hide something longer, store the text somewhere and embed a short key
18
+ instead; that is what [aurastamp.com](https://aurastamp.com) does.
19
+
20
+ ## Usage
21
+
22
+ ```python
23
+ from aurastamp_sdk import Aurastamp
24
+
25
+ aurastamp = Aurastamp() # or Aurastamp("http://localhost:8000")
26
+
27
+ # Hide a message
28
+ encoded = aurastamp.image.encode_file(
29
+ image_file_path="photo.png",
30
+ message="hello",
31
+ output_path="encoded.png",
32
+ )
33
+
34
+ # Reveal it again
35
+ print(aurastamp.image.decode_file(image_file_path="encoded.png"))
36
+ #=> ' hello' (padded to 7 bytes)
37
+ ```
38
+
39
+ Working with bytes directly:
40
+
41
+ ```python
42
+ with open("photo.png", "rb") as f:
43
+ encoded = aurastamp.image.encode(image=f.read(), message="hello", filename="photo.png")
44
+
45
+ message = aurastamp.image.decode(image=encoded, filename="encoded.png")
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ | Setting | Default |
51
+ | ------------------ | --------------------------- |
52
+ | `server_url` arg | `AURASTAMP_API_URL` env var |
53
+ | `AURASTAMP_API_URL`| `https://api.aurastamp.com` |
54
+
55
+ ## Errors
56
+
57
+ Every failure raises `AurastampError`, carrying the server's `detail` message
58
+ and the HTTP status where there is one.
59
+
60
+ ```python
61
+ from aurastamp_sdk import AurastampError
62
+
63
+ try:
64
+ aurastamp.image.encode_file(image_file_path="photo.png", message="way too long")
65
+ except AurastampError as error:
66
+ print(error, error.status)
67
+ ```
68
+
69
+ ## Coming from the `aurastamp` package
70
+
71
+ This supersedes `aurastamp` on PyPI, which is unmaintained and published from an
72
+ account this project no longer controls. Its last release could not talk to the
73
+ current API: it base64-decoded a response that was already raw PNG bytes and
74
+ read keys off the decode response that the server never sends.
75
+
76
+ The import is `aurastamp_sdk`, not `aurastamp` — two distributions installing
77
+ the same top-level package would overwrite each other's files.
78
+
79
+ If you depend on `aurastamp`, switch to `aurastamp-sdk`. What changed:
80
+
81
+ - `encode` / `encode_local_file` → `encode` / `encode_file`, and they now return
82
+ **PNG bytes** rather than a `requests.Response`
83
+ - `decode` / `decode_local_file` → `decode` / `decode_file`, returning the
84
+ message **string**
85
+ - the unused `model`, `return_type` and `hidden_image` arguments are gone
86
+ - failures raise `AurastampError` instead of returning a non-2xx response
@@ -0,0 +1,16 @@
1
+ from .aurastamp_image import (
2
+ DEFAULT_API_URL,
3
+ MAX_MESSAGE_BYTES,
4
+ AurastampError,
5
+ AurastampImage,
6
+ )
7
+ from .main import Aurastamp
8
+
9
+ __all__ = [
10
+ "Aurastamp",
11
+ "AurastampError",
12
+ "AurastampImage",
13
+ "DEFAULT_API_URL",
14
+ "MAX_MESSAGE_BYTES",
15
+ ]
16
+ __version__ = "2.0.0"
@@ -0,0 +1,130 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import requests
5
+
6
+ DEFAULT_API_URL = "https://api.aurastamp.com"
7
+
8
+ # The steganographic payload is exactly 7 bytes wide.
9
+ MAX_MESSAGE_BYTES = 7
10
+
11
+ _CONTENT_TYPES = {
12
+ ".png": "image/png",
13
+ ".jpg": "image/jpeg",
14
+ ".jpeg": "image/jpeg",
15
+ }
16
+
17
+
18
+ class AurastampError(Exception):
19
+ def __init__(self, message: str, status: int | None = None):
20
+ super().__init__(message)
21
+ self.status = status
22
+
23
+
24
+ def _content_type_for(filename: str) -> str:
25
+ content_type = _CONTENT_TYPES.get(Path(filename).suffix.lower())
26
+ if content_type is None:
27
+ raise AurastampError(
28
+ f"unsupported image type: {filename}. only png and jpeg are supported"
29
+ )
30
+ return content_type
31
+
32
+
33
+ def _describe(response: requests.Response) -> str:
34
+ try:
35
+ detail = response.json().get("detail")
36
+ if detail:
37
+ return detail
38
+ except ValueError:
39
+ pass
40
+ return f"request failed with status {response.status_code}"
41
+
42
+
43
+ class AurastampImage:
44
+ def __init__(self, server_url: str | None = None):
45
+ self.server_url = (
46
+ server_url or os.getenv("AURASTAMP_API_URL") or DEFAULT_API_URL
47
+ ).rstrip("/")
48
+
49
+ def encode(
50
+ self,
51
+ image: bytes,
52
+ message: str,
53
+ filename: str = "image.png",
54
+ content_type: str | None = None,
55
+ headers: dict | None = None,
56
+ timeout: float = 120,
57
+ ) -> bytes:
58
+ """Hide `message` inside `image` and return the stamped PNG bytes."""
59
+ byte_length = len(message.encode("utf-8"))
60
+ if byte_length > MAX_MESSAGE_BYTES:
61
+ raise AurastampError(
62
+ f"message is {byte_length} bytes; "
63
+ f"the payload holds at most {MAX_MESSAGE_BYTES}"
64
+ )
65
+
66
+ response = requests.post(
67
+ f"{self.server_url}/encode",
68
+ files={"file": (filename, image, content_type or _content_type_for(filename))},
69
+ data={"message": message},
70
+ headers=headers or {},
71
+ timeout=timeout,
72
+ )
73
+ if not response.ok:
74
+ raise AurastampError(_describe(response), response.status_code)
75
+ # The endpoint returns the PNG itself, not a base64 payload.
76
+ return response.content
77
+
78
+ def decode(
79
+ self,
80
+ image: bytes,
81
+ filename: str = "image.png",
82
+ content_type: str | None = None,
83
+ headers: dict | None = None,
84
+ timeout: float = 120,
85
+ ) -> str:
86
+ """Recover the message hidden in `image`."""
87
+ response = requests.post(
88
+ f"{self.server_url}/decode",
89
+ files={"file": (filename, image, content_type or _content_type_for(filename))},
90
+ headers=headers or {},
91
+ timeout=timeout,
92
+ )
93
+ if not response.ok:
94
+ raise AurastampError(_describe(response), response.status_code)
95
+ # The endpoint returns the message as a bare JSON string.
96
+ return response.json()
97
+
98
+ def encode_file(
99
+ self,
100
+ image_file_path: str,
101
+ message: str,
102
+ output_path: str | None = None,
103
+ headers: dict | None = None,
104
+ timeout: float = 120,
105
+ ) -> bytes:
106
+ source = Path(image_file_path)
107
+ encoded = self.encode(
108
+ image=source.read_bytes(),
109
+ message=message,
110
+ filename=source.name,
111
+ headers=headers,
112
+ timeout=timeout,
113
+ )
114
+ if output_path:
115
+ Path(output_path).write_bytes(encoded)
116
+ return encoded
117
+
118
+ def decode_file(
119
+ self,
120
+ image_file_path: str,
121
+ headers: dict | None = None,
122
+ timeout: float = 120,
123
+ ) -> str:
124
+ source = Path(image_file_path)
125
+ return self.decode(
126
+ image=source.read_bytes(),
127
+ filename=source.name,
128
+ headers=headers,
129
+ timeout=timeout,
130
+ )
@@ -0,0 +1,6 @@
1
+ from .aurastamp_image import AurastampImage
2
+
3
+
4
+ class Aurastamp:
5
+ def __init__(self, server_url: str | None = None):
6
+ self.image = AurastampImage(server_url)
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aurastamp-sdk"
7
+ version = "1.0.0"
8
+ description = "Hide and reveal short messages inside images, via the Aurastamp API"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "sunutf" }]
12
+ keywords = ["aurastamp", "steganography", "watermark", "image"]
13
+ requires-python = ">=3.10"
14
+ dependencies = ["requests>=2.31"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Topic :: Multimedia :: Graphics",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://aurastamp.com"
23
+ Repository = "https://github.com/sunutf/aurastamp"
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=8", "responses>=0.25"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["aurastamp_sdk"]
@@ -0,0 +1,62 @@
1
+ import pytest
2
+ import responses
3
+
4
+ from aurastamp_sdk import Aurastamp, AurastampError
5
+
6
+ API = "https://api.test"
7
+
8
+
9
+ @responses.activate
10
+ def test_encode_posts_message_and_returns_png_bytes():
11
+ responses.add(responses.POST, f"{API}/encode", body=b"\x89PNG", status=200)
12
+
13
+ result = Aurastamp(API).image.encode(image=b"fake", message="abc", filename="a.png")
14
+
15
+ assert result == b"\x89PNG"
16
+ request = responses.calls[0].request
17
+ # The server reads `message`, and returns raw PNG bytes rather than base64.
18
+ assert b'name="message"' in request.body
19
+ assert b"abc" in request.body
20
+
21
+
22
+ @responses.activate
23
+ def test_decode_returns_the_message():
24
+ responses.add(responses.POST, f"{API}/decode", json=" hello", status=200)
25
+
26
+ assert Aurastamp(API).image.decode(image=b"fake", filename="a.png") == " hello"
27
+
28
+
29
+ def test_message_longer_than_the_payload_is_rejected():
30
+ with pytest.raises(AurastampError, match="8 bytes"):
31
+ Aurastamp(API).image.encode(image=b"fake", message="12345678", filename="a.png")
32
+
33
+
34
+ def test_message_is_measured_in_bytes_not_characters():
35
+ # '가나다' is 3 characters but 9 utf-8 bytes.
36
+ with pytest.raises(AurastampError, match="9 bytes"):
37
+ Aurastamp(API).image.encode(image=b"fake", message="가나다", filename="a.png")
38
+
39
+
40
+ def test_unsupported_image_type_is_rejected():
41
+ with pytest.raises(AurastampError, match="only png and jpeg"):
42
+ Aurastamp(API).image.encode(image=b"fake", message="ok", filename="a.gif")
43
+
44
+
45
+ @responses.activate
46
+ def test_server_error_detail_is_surfaced():
47
+ responses.add(
48
+ responses.POST, f"{API}/encode", json={"detail": "could not read image"}, status=422
49
+ )
50
+
51
+ with pytest.raises(AurastampError, match="could not read image"):
52
+ Aurastamp(API).image.encode(image=b"fake", message="ok", filename="a.png")
53
+
54
+
55
+ def test_server_url_falls_back_to_the_environment(monkeypatch):
56
+ monkeypatch.setenv("AURASTAMP_API_URL", "https://from-env.test")
57
+ assert Aurastamp().image.server_url == "https://from-env.test"
58
+
59
+
60
+ def test_explicit_server_url_wins(monkeypatch):
61
+ monkeypatch.setenv("AURASTAMP_API_URL", "https://from-env.test")
62
+ assert Aurastamp("https://explicit.test/").image.server_url == "https://explicit.test"