sqr-cli 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.
- sqr_cli-0.1.0/PKG-INFO +9 -0
- sqr_cli-0.1.0/pyproject.toml +18 -0
- sqr_cli-0.1.0/setup.cfg +4 -0
- sqr_cli-0.1.0/sqr_cli/__init__.py +3 -0
- sqr_cli-0.1.0/sqr_cli/__main__.py +4 -0
- sqr_cli-0.1.0/sqr_cli/main.py +375 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/PKG-INFO +9 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/SOURCES.txt +10 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/dependency_links.txt +1 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/entry_points.txt +2 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/requires.txt +1 -0
- sqr_cli-0.1.0/sqr_cli.egg-info/top_level.txt +1 -0
sqr_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqr-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the YYS-SQR watermarking & digital product passport API
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://yys-sqr-render-bsbe.onrender.com
|
|
7
|
+
Project-URL: Documentation, https://yys-sqr-render-bsbe.onrender.com/api/v2/docs
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: requests>=2.28.0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqr-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI for the YYS-SQR watermarking & digital product passport API"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
dependencies = ["requests>=2.28.0"]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
sqr = "sqr_cli.main:main"
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://yys-sqr-render-bsbe.onrender.com"
|
|
18
|
+
Documentation = "https://yys-sqr-render-bsbe.onrender.com/api/v2/docs"
|
sqr_cli-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
YYS-SQR CLI — command-line client for the YYS-SQR API.
|
|
4
|
+
|
|
5
|
+
Install: pip install sqr-cli
|
|
6
|
+
Config: ~/.sqr/config.json
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
|
|
18
|
+
CONFIG_DIR = os.path.expanduser("~/.sqr")
|
|
19
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
20
|
+
DEFAULT_API_URL = "https://yys-sqr-render-bsbe.onrender.com"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Config helpers
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
def load_config():
|
|
28
|
+
if os.path.exists(CONFIG_FILE):
|
|
29
|
+
with open(CONFIG_FILE) as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def save_config(cfg):
|
|
35
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
36
|
+
with open(CONFIG_FILE, "w") as f:
|
|
37
|
+
json.dump(cfg, f, indent=2)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_api_url():
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
return cfg.get("api_url", DEFAULT_API_URL).rstrip("/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_headers():
|
|
46
|
+
cfg = load_config()
|
|
47
|
+
headers = {"Content-Type": "application/json"}
|
|
48
|
+
key = cfg.get("api_key")
|
|
49
|
+
if key:
|
|
50
|
+
headers["Authorization"] = f"Bearer {key}"
|
|
51
|
+
return headers
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# HTTP helpers
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
def _request(method, path, **kwargs):
|
|
59
|
+
url = f"{get_api_url()}{path}"
|
|
60
|
+
headers = kwargs.pop("headers", get_headers())
|
|
61
|
+
try:
|
|
62
|
+
resp = getattr(requests, method)(url, headers=headers, timeout=60, **kwargs)
|
|
63
|
+
except requests.ConnectionError:
|
|
64
|
+
print(f"Error: could not connect to {url}")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
except requests.Timeout:
|
|
67
|
+
print("Error: request timed out")
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
body = resp.json()
|
|
72
|
+
except ValueError:
|
|
73
|
+
body = resp.text
|
|
74
|
+
|
|
75
|
+
if resp.status_code >= 400:
|
|
76
|
+
err = body.get("error", body) if isinstance(body, dict) else body
|
|
77
|
+
print(f"Error ({resp.status_code}): {err}")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
return body
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _print_json(data):
|
|
84
|
+
print(json.dumps(data, indent=2))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Commands
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
def cmd_configure(args):
|
|
92
|
+
cfg = load_config()
|
|
93
|
+
api_url = input(f"API URL [{cfg.get('api_url', DEFAULT_API_URL)}]: ").strip()
|
|
94
|
+
if api_url:
|
|
95
|
+
cfg["api_url"] = api_url
|
|
96
|
+
elif "api_url" not in cfg:
|
|
97
|
+
cfg["api_url"] = DEFAULT_API_URL
|
|
98
|
+
|
|
99
|
+
api_key = input(f"API key [{cfg.get('api_key', '')}]: ").strip()
|
|
100
|
+
if api_key:
|
|
101
|
+
cfg["api_key"] = api_key
|
|
102
|
+
|
|
103
|
+
save_config(cfg)
|
|
104
|
+
print(f"Config saved to {CONFIG_FILE}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_health(args):
|
|
108
|
+
_print_json(_request("get", "/api/health"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# -- Templates ---------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def cmd_templates_list(args):
|
|
114
|
+
_print_json(_request("get", "/api/templates"))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_templates_get(args):
|
|
118
|
+
_print_json(_request("get", f"/api/templates/{args.id}"))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# -- Records -----------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def cmd_records_list(args):
|
|
124
|
+
params = {}
|
|
125
|
+
if args.page:
|
|
126
|
+
params["page"] = args.page
|
|
127
|
+
if args.per_page:
|
|
128
|
+
params["per_page"] = args.per_page
|
|
129
|
+
_print_json(_request("get", "/api/records", params=params))
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_records_get(args):
|
|
133
|
+
_print_json(_request("get", f"/api/records/{args.id}"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def cmd_records_create(args):
|
|
137
|
+
if not args.image:
|
|
138
|
+
print("Error: --image / -i is required")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
img_path = args.image
|
|
142
|
+
if not os.path.isfile(img_path):
|
|
143
|
+
print(f"Error: file not found: {img_path}")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
with open(img_path, "rb") as f:
|
|
147
|
+
image_b64 = base64.b64encode(f.read()).decode()
|
|
148
|
+
|
|
149
|
+
metadata = {}
|
|
150
|
+
for kv in (args.set or []):
|
|
151
|
+
if "=" not in kv:
|
|
152
|
+
print(f"Error: --set value must be key=value, got: {kv}")
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
k, v = kv.split("=", 1)
|
|
155
|
+
metadata[k] = v
|
|
156
|
+
|
|
157
|
+
payload = {
|
|
158
|
+
"image": image_b64,
|
|
159
|
+
"template_id": args.template or "custom",
|
|
160
|
+
"metadata": metadata,
|
|
161
|
+
}
|
|
162
|
+
_print_json(_request("post", "/api/records", json=payload))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def cmd_records_update(args):
|
|
166
|
+
metadata = {}
|
|
167
|
+
for kv in (args.set or []):
|
|
168
|
+
if "=" not in kv:
|
|
169
|
+
print(f"Error: --set value must be key=value, got: {kv}")
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
k, v = kv.split("=", 1)
|
|
172
|
+
metadata[k] = v
|
|
173
|
+
|
|
174
|
+
if not metadata:
|
|
175
|
+
print("Error: provide at least one --set key=value")
|
|
176
|
+
sys.exit(1)
|
|
177
|
+
|
|
178
|
+
_print_json(_request("patch", f"/api/records/{args.id}", json={"metadata": metadata}))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# -- Scan / Embed ------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def cmd_scan(args):
|
|
184
|
+
img_path = args.image_path
|
|
185
|
+
if not os.path.isfile(img_path):
|
|
186
|
+
print(f"Error: file not found: {img_path}")
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
with open(img_path, "rb") as f:
|
|
190
|
+
image_b64 = base64.b64encode(f.read()).decode()
|
|
191
|
+
|
|
192
|
+
payload = {"image": image_b64}
|
|
193
|
+
if args.corners:
|
|
194
|
+
try:
|
|
195
|
+
corners = json.loads(args.corners)
|
|
196
|
+
except json.JSONDecodeError:
|
|
197
|
+
print("Error: --corners must be valid JSON, e.g. '[[0,0],[100,0],[100,100],[0,100]]'")
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
payload["corners"] = corners
|
|
200
|
+
|
|
201
|
+
_print_json(_request("post", "/api/scan", json=payload))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def cmd_embed(args):
|
|
205
|
+
if not args.image:
|
|
206
|
+
print("Error: --image / -i is required")
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
if not args.message:
|
|
209
|
+
print("Error: --message / -m is required")
|
|
210
|
+
sys.exit(1)
|
|
211
|
+
|
|
212
|
+
img_path = args.image
|
|
213
|
+
if not os.path.isfile(img_path):
|
|
214
|
+
print(f"Error: file not found: {img_path}")
|
|
215
|
+
sys.exit(1)
|
|
216
|
+
|
|
217
|
+
with open(img_path, "rb") as f:
|
|
218
|
+
image_b64 = base64.b64encode(f.read()).decode()
|
|
219
|
+
|
|
220
|
+
result = _request("post", "/api/embed", json={"image": image_b64, "message": args.message})
|
|
221
|
+
|
|
222
|
+
# Save watermarked image if present
|
|
223
|
+
if args.output and isinstance(result, dict) and result.get("watermarked_image"):
|
|
224
|
+
with open(args.output, "wb") as f:
|
|
225
|
+
f.write(base64.b64decode(result["watermarked_image"]))
|
|
226
|
+
print(f"Watermarked image saved to {args.output}")
|
|
227
|
+
result.pop("watermarked_image", None)
|
|
228
|
+
|
|
229
|
+
_print_json(result)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# -- Keys -------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def cmd_keys_create(args):
|
|
235
|
+
payload = {"name": args.name or "default"}
|
|
236
|
+
_print_json(_request("post", "/api/keys", json=payload))
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def cmd_keys_list(args):
|
|
240
|
+
_print_json(_request("get", "/api/keys"))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def cmd_keys_revoke(args):
|
|
244
|
+
_print_json(_request("post", f"/api/keys/{args.id}/revoke"))
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ---------------------------------------------------------------------------
|
|
248
|
+
# Argument parser
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def build_parser():
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
prog="sqr",
|
|
254
|
+
description="YYS-SQR CLI — watermarking & digital product passport platform",
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
257
|
+
sub = parser.add_subparsers(dest="command")
|
|
258
|
+
|
|
259
|
+
# configure
|
|
260
|
+
sub.add_parser("configure", help="Save API URL + key to ~/.sqr/config.json")
|
|
261
|
+
|
|
262
|
+
# health
|
|
263
|
+
sub.add_parser("health", help="GET /api/health")
|
|
264
|
+
|
|
265
|
+
# templates
|
|
266
|
+
tmpl = sub.add_parser("templates", help="Template commands")
|
|
267
|
+
tmpl_sub = tmpl.add_subparsers(dest="templates_cmd")
|
|
268
|
+
tmpl_sub.add_parser("list", help="List all templates")
|
|
269
|
+
tg = tmpl_sub.add_parser("get", help="Get template details")
|
|
270
|
+
tg.add_argument("id", help="Template ID")
|
|
271
|
+
|
|
272
|
+
# records
|
|
273
|
+
rec = sub.add_parser("records", help="Record commands")
|
|
274
|
+
rec_sub = rec.add_subparsers(dest="records_cmd")
|
|
275
|
+
|
|
276
|
+
rl = rec_sub.add_parser("list", help="List records")
|
|
277
|
+
rl.add_argument("--page", type=int)
|
|
278
|
+
rl.add_argument("--per-page", type=int, dest="per_page")
|
|
279
|
+
|
|
280
|
+
rg = rec_sub.add_parser("get", help="Get a record")
|
|
281
|
+
rg.add_argument("id", help="Watermark ID")
|
|
282
|
+
|
|
283
|
+
rc = rec_sub.add_parser("create", help="Create a record")
|
|
284
|
+
rc.add_argument("-t", "--template", default="custom", help="Template ID")
|
|
285
|
+
rc.add_argument("-i", "--image", required=True, help="Path to image file")
|
|
286
|
+
rc.add_argument("--set", action="append", metavar="KEY=VALUE", help="Metadata key=value")
|
|
287
|
+
|
|
288
|
+
ru = rec_sub.add_parser("update", help="Update record metadata")
|
|
289
|
+
ru.add_argument("id", help="Watermark ID")
|
|
290
|
+
ru.add_argument("--set", action="append", metavar="KEY=VALUE", help="Metadata key=value")
|
|
291
|
+
|
|
292
|
+
# scan
|
|
293
|
+
sc = sub.add_parser("scan", help="Scan an image for watermarks")
|
|
294
|
+
sc.add_argument("image_path", help="Path to image file")
|
|
295
|
+
sc.add_argument("--corners", help="JSON array of 4 corner points")
|
|
296
|
+
|
|
297
|
+
# embed
|
|
298
|
+
em = sub.add_parser("embed", help="Embed watermark into image")
|
|
299
|
+
em.add_argument("-i", "--image", required=True, help="Path to image file")
|
|
300
|
+
em.add_argument("-m", "--message", required=True, help="Message to embed (max 5 chars)")
|
|
301
|
+
em.add_argument("-o", "--output", help="Save watermarked image to file")
|
|
302
|
+
|
|
303
|
+
# keys
|
|
304
|
+
keys = sub.add_parser("keys", help="API key management")
|
|
305
|
+
keys_sub = keys.add_subparsers(dest="keys_cmd")
|
|
306
|
+
kc = keys_sub.add_parser("create", help="Create a new API key")
|
|
307
|
+
kc.add_argument("--name", default="default", help="Key name")
|
|
308
|
+
keys_sub.add_parser("list", help="List your API keys")
|
|
309
|
+
kr = keys_sub.add_parser("revoke", help="Revoke an API key")
|
|
310
|
+
kr.add_argument("id", help="Key ID")
|
|
311
|
+
|
|
312
|
+
return parser
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------------------
|
|
316
|
+
# Main
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
def main():
|
|
320
|
+
parser = build_parser()
|
|
321
|
+
args = parser.parse_args()
|
|
322
|
+
|
|
323
|
+
if not args.command:
|
|
324
|
+
parser.print_help()
|
|
325
|
+
sys.exit(0)
|
|
326
|
+
|
|
327
|
+
dispatch = {
|
|
328
|
+
"configure": cmd_configure,
|
|
329
|
+
"health": cmd_health,
|
|
330
|
+
"scan": cmd_scan,
|
|
331
|
+
"embed": cmd_embed,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if args.command in dispatch:
|
|
335
|
+
dispatch[args.command](args)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if args.command == "templates":
|
|
339
|
+
if args.templates_cmd == "list":
|
|
340
|
+
cmd_templates_list(args)
|
|
341
|
+
elif args.templates_cmd == "get":
|
|
342
|
+
cmd_templates_get(args)
|
|
343
|
+
else:
|
|
344
|
+
print("Usage: sqr templates {list,get}")
|
|
345
|
+
sys.exit(1)
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
if args.command == "records":
|
|
349
|
+
cmds = {
|
|
350
|
+
"list": cmd_records_list,
|
|
351
|
+
"get": cmd_records_get,
|
|
352
|
+
"create": cmd_records_create,
|
|
353
|
+
"update": cmd_records_update,
|
|
354
|
+
}
|
|
355
|
+
if args.records_cmd in cmds:
|
|
356
|
+
cmds[args.records_cmd](args)
|
|
357
|
+
else:
|
|
358
|
+
print("Usage: sqr records {list,get,create,update}")
|
|
359
|
+
sys.exit(1)
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
if args.command == "keys":
|
|
363
|
+
cmds = {
|
|
364
|
+
"create": cmd_keys_create,
|
|
365
|
+
"list": cmd_keys_list,
|
|
366
|
+
"revoke": cmd_keys_revoke,
|
|
367
|
+
}
|
|
368
|
+
if args.keys_cmd in cmds:
|
|
369
|
+
cmds[args.keys_cmd](args)
|
|
370
|
+
else:
|
|
371
|
+
print("Usage: sqr keys {create,list,revoke}")
|
|
372
|
+
sys.exit(1)
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
parser.print_help()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqr-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for the YYS-SQR watermarking & digital product passport API
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://yys-sqr-render-bsbe.onrender.com
|
|
7
|
+
Project-URL: Documentation, https://yys-sqr-render-bsbe.onrender.com/api/v2/docs
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: requests>=2.28.0
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
sqr_cli/__init__.py
|
|
3
|
+
sqr_cli/__main__.py
|
|
4
|
+
sqr_cli/main.py
|
|
5
|
+
sqr_cli.egg-info/PKG-INFO
|
|
6
|
+
sqr_cli.egg-info/SOURCES.txt
|
|
7
|
+
sqr_cli.egg-info/dependency_links.txt
|
|
8
|
+
sqr_cli.egg-info/entry_points.txt
|
|
9
|
+
sqr_cli.egg-info/requires.txt
|
|
10
|
+
sqr_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.28.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sqr_cli
|