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.
- aurastamp_sdk-1.0.0/.gitignore +13 -0
- aurastamp_sdk-1.0.0/PKG-INFO +105 -0
- aurastamp_sdk-1.0.0/README.md +86 -0
- aurastamp_sdk-1.0.0/aurastamp_sdk/__init__.py +16 -0
- aurastamp_sdk-1.0.0/aurastamp_sdk/aurastamp_image.py +130 -0
- aurastamp_sdk-1.0.0/aurastamp_sdk/main.py +6 -0
- aurastamp_sdk-1.0.0/pyproject.toml +29 -0
- aurastamp_sdk-1.0.0/tests/test_aurastamp.py +62 -0
|
@@ -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,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"
|