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 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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """YYS-SQR CLI — command-line client for the YYS-SQR API."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ """Allow running as `python -m sqr_cli`."""
2
+ from .main import main
3
+
4
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ sqr = sqr_cli.main:main
@@ -0,0 +1 @@
1
+ requests>=2.28.0
@@ -0,0 +1 @@
1
+ sqr_cli