unitysvc-data 0.1.0__py3-none-any.whl

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,119 @@
1
+ """Standard examples and presets for UnitySVC data packages.
2
+
3
+ Pure-data package: ships example files under ``examples/`` organised by
4
+ gateway and preset family, preset factories that return populated
5
+ document records, and a JSON sentinel walker the sellers SDK calls at
6
+ upload time. The same machinery is intended to serve other parts of the
7
+ platform that need versioned, named references to bundled content.
8
+
9
+ Preferred (preset-name) API:
10
+
11
+ - :func:`doc_preset` — return a full document record for a preset,
12
+ from a bare name or a ``{"$preset": ..., "$with": {...}}`` sentinel.
13
+ - :func:`file_preset` — return the raw UTF-8 content of a preset's
14
+ bundled example file.
15
+ - :func:`list_presets` — enumerate every registered preset name
16
+ (versioned + aliases).
17
+ - :data:`PRESETS` — mapping of preset name → factory function.
18
+ - :data:`ALIASES` — mapping of version-less family name → latest
19
+ versioned preset name.
20
+ - :data:`MANIFEST` — parsed ``_manifest.json`` content.
21
+ - :data:`OVERRIDABLE` — fields that may be overridden in ``$with``.
22
+ - :func:`register_jinja_globals` — expose every preset factory as a
23
+ Jinja2 global (for generated repos that render ``listing.json.j2``).
24
+
25
+ Low-level (path-based) API — prefer the preset API above unless you
26
+ specifically need to address files by their on-disk layout:
27
+
28
+ - :func:`example_path`, :func:`read_example`, :func:`list_examples` —
29
+ resolve and read bundled example files by their path under
30
+ ``examples/``. These break if we ever reorganise the tree; preset
31
+ names don't.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from importlib.resources import files as _files
37
+ from pathlib import Path
38
+
39
+ from ._version import __version__
40
+
41
+ _ROOT = _files(__name__).joinpath("examples")
42
+
43
+
44
+ def example_path(name: str) -> Path:
45
+ """Return an absolute filesystem path for the bundled example *name*.
46
+
47
+ **Low-level.** Prefer :func:`file_preset` and :func:`doc_preset`
48
+ for anything that can key off a preset name — those are stable
49
+ across any future reorganisation of ``examples/``.
50
+
51
+ *name* is a path relative to ``examples/`` (e.g.
52
+ ``"s3/connectivity/connectivity-v1.py.j2"``). Raises
53
+ :class:`FileNotFoundError` if the file is not part of the
54
+ installed package.
55
+ """
56
+ target = _ROOT.joinpath(name)
57
+ if not target.is_file():
58
+ raise FileNotFoundError(f"Unknown example: {name!r}")
59
+ return Path(str(target))
60
+
61
+
62
+ def read_example(name: str) -> str:
63
+ """Read the raw UTF-8 content of a bundled example by path.
64
+
65
+ **Low-level.** See :func:`example_path` for the name contract and
66
+ why :func:`file_preset` is usually the right choice.
67
+ """
68
+ return example_path(name).read_text(encoding="utf-8")
69
+
70
+
71
+ def list_examples() -> list[str]:
72
+ """Return every bundled example filename, relative to ``examples/``.
73
+
74
+ **Low-level.** For enumerating presets by name, use
75
+ :func:`list_presets`. This function exposes on-disk paths
76
+ (including the gateway/family structure), which is rarely what a
77
+ consumer wants.
78
+
79
+ README files and hidden files are excluded.
80
+ """
81
+ root = Path(str(_ROOT))
82
+ return sorted(
83
+ path.relative_to(root).as_posix()
84
+ for path in root.rglob("*")
85
+ if path.is_file()
86
+ and path.name != "README.md"
87
+ and not path.name.startswith(".")
88
+ )
89
+
90
+
91
+ # Imported last so presets.py can compute its own _EXAMPLES_ROOT without
92
+ # reaching back into this module while __init__ is still loading.
93
+ from .presets import ( # noqa: E402 (placement is deliberate)
94
+ ALIASES,
95
+ MANIFEST,
96
+ OVERRIDABLE,
97
+ PRESETS,
98
+ doc_preset,
99
+ file_preset,
100
+ list_presets,
101
+ register_jinja_globals,
102
+ )
103
+
104
+ __all__ = [
105
+ "__version__",
106
+ # Preferred preset API.
107
+ "doc_preset",
108
+ "file_preset",
109
+ "list_presets",
110
+ "PRESETS",
111
+ "ALIASES",
112
+ "MANIFEST",
113
+ "OVERRIDABLE",
114
+ "register_jinja_globals",
115
+ # Low-level path-based API.
116
+ "example_path",
117
+ "read_example",
118
+ "list_examples",
119
+ ]
@@ -0,0 +1,91 @@
1
+ {
2
+ "aliases": {
3
+ "api_connectivity": "api_connectivity_v1",
4
+ "llm_request_template": "llm_request_template_v1",
5
+ "s3_code_example": "s3_code_example_v1",
6
+ "s3_connectivity": "s3_connectivity_v1",
7
+ "s3_description": "s3_description_v1",
8
+ "smtp_connectivity": "smtp_connectivity_v1"
9
+ },
10
+ "presets": {
11
+ "api_connectivity_v1": {
12
+ "category": "connectivity_test",
13
+ "description": "Verify HTTP endpoint is reachable and returns a healthy status",
14
+ "example_file": "api/connectivity/connectivity-v1.sh.j2",
15
+ "is_active": true,
16
+ "is_public": false,
17
+ "meta": {
18
+ "output_contains": "connectivity ok"
19
+ },
20
+ "mime_type": "bash",
21
+ "preset_name": "api_connectivity",
22
+ "source_readme": "api/connectivity/README.md",
23
+ "version": 1
24
+ },
25
+ "llm_request_template_v1": {
26
+ "category": "request_template",
27
+ "description": "Minimal OpenAI-compatible chat completion request body",
28
+ "example_file": "llm/request-template/request-template-v1.json",
29
+ "is_active": true,
30
+ "is_public": false,
31
+ "meta": {},
32
+ "mime_type": "json",
33
+ "preset_name": "llm_request_template",
34
+ "source_readme": "llm/request-template/README.md",
35
+ "version": 1
36
+ },
37
+ "s3_code_example_v1": {
38
+ "category": "usage_example",
39
+ "description": "Python example: list objects in an S3 bucket via boto3",
40
+ "example_file": "s3/code-example/code-example-v1.py.j2",
41
+ "is_active": true,
42
+ "is_public": true,
43
+ "meta": {},
44
+ "mime_type": "python",
45
+ "preset_name": "s3_code_example",
46
+ "source_readme": "s3/code-example/README.md",
47
+ "version": 1
48
+ },
49
+ "s3_connectivity_v1": {
50
+ "category": "connectivity_test",
51
+ "description": "Verify S3 endpoint accepts the configured credentials",
52
+ "example_file": "s3/connectivity/connectivity-v1.py.j2",
53
+ "is_active": true,
54
+ "is_public": false,
55
+ "meta": {
56
+ "output_contains": "connectivity ok"
57
+ },
58
+ "mime_type": "python",
59
+ "preset_name": "s3_connectivity",
60
+ "source_readme": "s3/connectivity/README.md",
61
+ "version": 1
62
+ },
63
+ "s3_description_v1": {
64
+ "category": "description",
65
+ "description": "Customer-facing overview of the S3 gateway service",
66
+ "example_file": "s3/description/description-v1.md",
67
+ "is_active": true,
68
+ "is_public": true,
69
+ "meta": {},
70
+ "mime_type": "markdown",
71
+ "preset_name": "s3_description",
72
+ "source_readme": "s3/description/README.md",
73
+ "version": 1
74
+ },
75
+ "smtp_connectivity_v1": {
76
+ "category": "connectivity_test",
77
+ "description": "Verify SMTP server returns a 220 greeting on connect",
78
+ "example_file": "smtp/connectivity/connectivity-v1.sh.j2",
79
+ "is_active": true,
80
+ "is_public": false,
81
+ "meta": {
82
+ "output_contains": "connectivity ok"
83
+ },
84
+ "mime_type": "bash",
85
+ "preset_name": "smtp_connectivity",
86
+ "source_readme": "smtp/connectivity/README.md",
87
+ "version": 1
88
+ }
89
+ },
90
+ "version": "1"
91
+ }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
unitysvc_data/cli.py ADDED
@@ -0,0 +1,222 @@
1
+ """``usvc_data`` — browse and expand bundled presets from the shell.
2
+
3
+ Subcommands:
4
+
5
+ - ``usvc_data list`` — print every preset name (versioned + aliases).
6
+ - ``usvc_data info <name>`` — print a preset's README prose (metadata
7
+ is shown on the list view / doc-preset output).
8
+ - ``usvc_data doc-preset <name> [--with JSON]`` — print the expanded
9
+ document record as JSON on stdout.
10
+ - ``usvc_data file-preset <name>`` — print the raw content of the
11
+ preset's bundled file on stdout. For ``.j2`` templates the raw
12
+ template source is returned — Jinja2 rendering is the caller's
13
+ responsibility (the sellers SDK does it per-listing).
14
+
15
+ Example:
16
+
17
+ $ usvc_data doc-preset s3_connectivity --with '{"description": "ours"}'
18
+ {
19
+ "category": "connectivity_test",
20
+ "description": "ours",
21
+ ...
22
+ }
23
+ $ usvc_data file-preset s3_connectivity > /tmp/smoke.py
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import re
31
+ import sys
32
+ from typing import Any
33
+
34
+ from . import ALIASES, MANIFEST, PRESETS, __version__, doc_preset, file_preset
35
+
36
+ # Matches a TOML front-matter block delimited by '+++' lines at the top
37
+ # of a README. Mirrors the parser in tools/build.py.
38
+ _FRONT_MATTER_RE = re.compile(r"^\+\+\+\s*\n(.*?)\n\+\+\+\s*\n", re.DOTALL)
39
+
40
+
41
+ def _parse_with(value: str | None) -> tuple[dict[str, Any] | None, str | None]:
42
+ """Parse the ``--with`` JSON argument.
43
+
44
+ Returns ``(overrides, None)`` on success or ``(None, error_message)``
45
+ on failure. Callers print the error to stderr and return 1 — this
46
+ keeps error handling symmetric with the other subcommands (all
47
+ errors flow through the ``main()`` return value; no ``SystemExit``
48
+ bypass).
49
+ """
50
+ if value is None:
51
+ return {}, None
52
+ try:
53
+ parsed = json.loads(value)
54
+ except json.JSONDecodeError as exc:
55
+ return None, f"--with must be a JSON object: {exc}"
56
+ if not isinstance(parsed, dict):
57
+ return None, f"--with must be a JSON object, got {type(parsed).__name__}"
58
+ return parsed, None
59
+
60
+
61
+ def _cmd_list(args: argparse.Namespace) -> int:
62
+ versioned = sorted(MANIFEST["presets"])
63
+ aliases = sorted(ALIASES)
64
+
65
+ if args.json:
66
+ json.dump(
67
+ {"versioned": versioned, "aliases": {a: ALIASES[a] for a in aliases}},
68
+ sys.stdout,
69
+ indent=2,
70
+ sort_keys=True,
71
+ )
72
+ sys.stdout.write("\n")
73
+ return 0
74
+
75
+ print(f"Versioned presets ({len(versioned)}):")
76
+ for name in versioned:
77
+ entry = MANIFEST["presets"][name]
78
+ print(f" {name:40s} {entry['category']:20s} {entry['mime_type']}")
79
+ print()
80
+ print(f"Aliases ({len(aliases)}):")
81
+ for alias in aliases:
82
+ print(f" {alias:40s} -> {ALIASES[alias]}")
83
+ return 0
84
+
85
+
86
+ def _cmd_info(args: argparse.Namespace) -> int:
87
+ """Print the README prose (front-matter stripped) for a preset."""
88
+ name = args.name
89
+ # Resolve via the same alias logic the runtime uses.
90
+ target = ALIASES.get(name, name)
91
+ entry = MANIFEST["presets"].get(target)
92
+ if entry is None:
93
+ print(
94
+ f"error: Unknown preset: {name!r}. Available: {sorted(set(MANIFEST['presets']) | set(ALIASES))!r}",
95
+ file=sys.stderr,
96
+ )
97
+ return 1
98
+
99
+ # source_readme is stored relative to examples/; resolve through
100
+ # the package's example_path so it works from any install layout.
101
+ from . import example_path
102
+
103
+ text = example_path(entry["source_readme"]).read_text(encoding="utf-8")
104
+ match = _FRONT_MATTER_RE.match(text)
105
+ prose = text[match.end():] if match else text
106
+ # Strip leading blank lines so the first visible line is the first
107
+ # heading / paragraph of the README.
108
+ sys.stdout.write(prose.lstrip("\n"))
109
+ if not prose.endswith("\n"):
110
+ sys.stdout.write("\n")
111
+ return 0
112
+
113
+
114
+ def _cmd_doc_preset(args: argparse.Namespace) -> int:
115
+ overrides, err = _parse_with(args.with_json)
116
+ if err is not None:
117
+ print(f"error: {err}", file=sys.stderr)
118
+ return 1
119
+ try:
120
+ record = doc_preset(args.name, **overrides)
121
+ except (KeyError, ValueError, TypeError) as exc:
122
+ print(f"error: {exc}", file=sys.stderr)
123
+ return 1
124
+
125
+ indent = None if args.compact else 2
126
+ json.dump(record, sys.stdout, indent=indent, sort_keys=args.sort_keys)
127
+ sys.stdout.write("\n")
128
+ return 0
129
+
130
+
131
+ def _cmd_file_preset(args: argparse.Namespace) -> int:
132
+ try:
133
+ content = file_preset(args.name)
134
+ except (KeyError, ValueError, TypeError) as exc:
135
+ print(f"error: {exc}", file=sys.stderr)
136
+ return 1
137
+ # Write exactly what the file contains — don't add/trim a trailing
138
+ # newline. Consumers piping to a file should get byte-identical
139
+ # content to the bundled source.
140
+ sys.stdout.write(content)
141
+ return 0
142
+
143
+
144
+ def _build_parser() -> argparse.ArgumentParser:
145
+ parser = argparse.ArgumentParser(
146
+ prog="usvc_data",
147
+ description="Browse and expand presets bundled with unitysvc-data.",
148
+ )
149
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
150
+
151
+ sub = parser.add_subparsers(dest="command", required=True, metavar="COMMAND")
152
+
153
+ p_list = sub.add_parser("list", help="List every bundled preset.")
154
+ p_list.add_argument("--json", action="store_true", help="Emit machine-readable JSON.")
155
+ p_list.set_defaults(func=_cmd_list)
156
+
157
+ p_info = sub.add_parser(
158
+ "info",
159
+ help="Print the README prose for a preset.",
160
+ description=(
161
+ "Print the human-readable description of a preset — the "
162
+ "family README with its TOML front-matter stripped off. "
163
+ "Handy for browsing what a preset does without opening the "
164
+ "GitHub tree."
165
+ ),
166
+ )
167
+ p_info.add_argument("name", help="Preset name (versioned or alias).")
168
+ p_info.set_defaults(func=_cmd_info)
169
+
170
+ p_doc = sub.add_parser(
171
+ "doc-preset",
172
+ help="Print the expanded document record for a preset as JSON.",
173
+ )
174
+ p_doc.add_argument("name", help="Preset name (versioned or alias).")
175
+ p_doc.add_argument(
176
+ "--with",
177
+ dest="with_json",
178
+ metavar="JSON",
179
+ help=(
180
+ "Per-field overrides as a JSON object. "
181
+ "Allowed keys: description, is_active, is_public, meta."
182
+ ),
183
+ )
184
+ p_doc.add_argument("--compact", action="store_true", help="Emit single-line JSON.")
185
+ p_doc.add_argument(
186
+ "--sort-keys",
187
+ action="store_true",
188
+ help="Sort JSON keys for stable diffs.",
189
+ )
190
+ p_doc.set_defaults(func=_cmd_doc_preset)
191
+
192
+ p_file = sub.add_parser(
193
+ "file-preset",
194
+ help="Print the raw content of a preset's bundled example file.",
195
+ description=(
196
+ "Print the raw content of a preset's bundled example file. "
197
+ "For .j2 templates the output is the raw template source — "
198
+ "Jinja2 constructs like '{% if ... %}' are preserved verbatim "
199
+ "and are expected to be rendered later by the SDK with "
200
+ "per-listing context. Piping the result to an executable "
201
+ "file only works cleanly for non-.j2 presets."
202
+ ),
203
+ )
204
+ p_file.add_argument("name", help="Preset name (versioned or alias).")
205
+ p_file.set_defaults(func=_cmd_file_preset)
206
+
207
+ return parser
208
+
209
+
210
+ def main(argv: list[str] | None = None) -> int:
211
+ parser = _build_parser()
212
+ args = parser.parse_args(argv)
213
+ return args.func(args)
214
+
215
+
216
+ # Silence unused-import warnings where PRESETS is only kept for debugging
217
+ # at the REPL via ``python -m unitysvc_data.cli``.
218
+ _ = PRESETS
219
+
220
+
221
+ if __name__ == "__main__":
222
+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1
+ +++
2
+ preset_name = "api_connectivity"
3
+ category = "connectivity_test"
4
+ mime_type = "bash"
5
+ file = "connectivity.sh.j2"
6
+ description = "Verify HTTP endpoint is reachable and returns a healthy status"
7
+ is_active = true
8
+ is_public = false
9
+ meta = { output_contains = "connectivity ok" }
10
+ +++
11
+
12
+ # api / connectivity — generic HTTP connectivity test
13
+
14
+ Bash smoke test for any HTTP-based service (echo relays, mock LLMs,
15
+ proxy variants, any endpoint fronted by an HTTP gateway). The script
16
+ `curl`s the endpoint and classifies the response.
17
+
18
+ ## Pass / fail classification
19
+
20
+ | HTTP status | Verdict | Rationale |
21
+ |-------------|---------|-----------|
22
+ | 2xx, 3xx | pass | endpoint responding normally |
23
+ | 401, 403 | pass | endpoint is alive and enforcing auth |
24
+ | 404 | fail | wrong path — misconfigured |
25
+ | 5xx | fail | upstream broken |
26
+ | 000 | fail | connect / DNS / timeout |
27
+
28
+ ## Environment variables
29
+
30
+ Provided identically by the test harness in both local and gateway
31
+ modes, so the script does not branch on `local_testing`:
32
+
33
+ - `SERVICE_BASE_URL` — upstream URL (local) or gateway URL (online).
34
+ - `UNITYSVC_API_KEY` — optional. Seller's upstream api key if present
35
+ in `upstream_access_config` (local mode), or the customer's gateway
36
+ key (online mode). Sent as `Authorization: Bearer ...` when set.
37
+
38
+ ## Versions
39
+
40
+ ### v1 — initial release
41
+
42
+ - Classifies 2xx/3xx/401/403 as pass; 404/5xx/000 as fail.
43
+ - Timeout: 5 seconds.
44
+ - Works under `set -u` and `set -o pipefail` on bash 3.2 (macOS
45
+ default).
@@ -0,0 +1,42 @@
1
+ #!/bin/bash
2
+ # Connectivity test for HTTP-based services (echo relay, mock LLM, proxy
3
+ # variants). Verifies the endpoint is reachable AND serves a sensible
4
+ # response. 404 / 5xx / DNS / timeout all count as failure.
5
+ #
6
+ # The harness sets the same env var names in both modes:
7
+ # $SERVICE_BASE_URL — upstream URL (local) or gateway URL (online)
8
+ # $UNITYSVC_API_KEY — upstream api_key if defined in upstream_access_config
9
+ # (local), or the customer's gateway key (online)
10
+ # Because the variable names are identical we don't branch on
11
+ # local_testing for this test — the script works in both modes.
12
+ set -o pipefail
13
+
14
+ URL="${SERVICE_BASE_URL:?SERVICE_BASE_URL is not set}"
15
+
16
+ # Split the curl invocation rather than expanding an empty array under
17
+ # `set -u` (bash 3.2 on macOS raises on "${arr[@]}" when arr is empty).
18
+ if [ -n "${UNITYSVC_API_KEY:-}" ]; then
19
+ status=$(curl -sS --max-time 5 \
20
+ -H "Authorization: Bearer ${UNITYSVC_API_KEY}" \
21
+ -o /dev/null -w "%{http_code}" "$URL" 2>/dev/null)
22
+ else
23
+ status=$(curl -sS --max-time 5 \
24
+ -o /dev/null -w "%{http_code}" "$URL" 2>/dev/null)
25
+ fi
26
+
27
+ # Accept 2xx/3xx (alive) and 401/403 (alive, auth challenge).
28
+ # Reject 000 (connect/DNS failure), 404 (wrong path), 5xx (broken).
29
+ case "$status" in
30
+ 2??|3??|401|403)
31
+ echo "connectivity ok (HTTP $status)"
32
+ exit 0
33
+ ;;
34
+ 000)
35
+ echo "connectivity failed: could not connect to $URL" >&2
36
+ exit 1
37
+ ;;
38
+ *)
39
+ echo "connectivity failed: HTTP $status from $URL" >&2
40
+ exit 1
41
+ ;;
42
+ esac
@@ -0,0 +1,50 @@
1
+ +++
2
+ preset_name = "llm_request_template"
3
+ category = "request_template"
4
+ mime_type = "json"
5
+ file = "request-template.json"
6
+ description = "Minimal OpenAI-compatible chat completion request body"
7
+ is_active = true
8
+ is_public = false
9
+ +++
10
+
11
+ # llm / request-template — minimal chat completion payload
12
+
13
+ Canonical JSON request body for OpenAI-compatible chat completion
14
+ services routed through the UnitySVC LLM gateway. Suitable as a test
15
+ payload for seller-side validation, and as `request_template`
16
+ metadata attached to a listing.
17
+
18
+ ## Body
19
+
20
+ ```json
21
+ {
22
+ "max_tokens": 100,
23
+ "messages": [
24
+ { "role": "system", "content": "You are a helpful assistant." },
25
+ { "role": "user", "content": "Say hello in one sentence." }
26
+ ]
27
+ }
28
+ ```
29
+
30
+ ## What's intentionally missing
31
+
32
+ - **No `model` field.** The gateway's routing config or the listing's
33
+ `upstream_access_config` selects the upstream model; hard-coding
34
+ one here would tie the template to a specific service.
35
+ - **No `stream` field.** Non-streaming responses are simpler to
36
+ validate in tests; streaming variants belong in separate presets.
37
+
38
+ ## Conventions
39
+
40
+ - `max_tokens` is kept small (≤ 100) so the request completes fast
41
+ against any upstream regardless of per-token latency.
42
+ - Response format is standard OpenAI `chat.completions`. Tests should
43
+ assert `choices[0].message.content` exists and is non-empty.
44
+
45
+ ## Versions
46
+
47
+ ### v1 — initial release
48
+
49
+ - Two messages (system + user), `max_tokens = 100`, no model field,
50
+ non-streaming.
@@ -0,0 +1,13 @@
1
+ {
2
+ "max_tokens": 100,
3
+ "messages": [
4
+ {
5
+ "content": "You are a helpful assistant.",
6
+ "role": "system"
7
+ },
8
+ {
9
+ "content": "Say hello in one sentence.",
10
+ "role": "user"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,49 @@
1
+ +++
2
+ preset_name = "s3_code_example"
3
+ category = "usage_example"
4
+ mime_type = "python"
5
+ file = "code-example.py.j2"
6
+ description = "Python example: list objects in an S3 bucket via boto3"
7
+ is_active = true
8
+ is_public = true
9
+ +++
10
+
11
+ # s3 / code-example — list objects via boto3
12
+
13
+ Customer-facing primary example for S3 gateway services. Lists up to
14
+ five objects from the bucket using `boto3` and prints their keys.
15
+
16
+ ## Branches (rendered at upload time with the listing context)
17
+
18
+ | `local_testing` | `interface.access_key` | Behaviour |
19
+ |-----------------|------------------------|-----------|
20
+ | `true` | present | Uses seller credentials + optional `S3_ENDPOINT`. |
21
+ | `true` | absent | Unsigned `boto3` client against a public bucket. |
22
+ | `false` | — | Gateway-routed with the customer's `UNITYSVC_API_KEY`. |
23
+
24
+ ## Environment variables (local mode)
25
+
26
+ - `REGION`, `ACCESS_KEY`, `SECRET_KEY` — seller credentials (only when
27
+ `interface.access_key` is set).
28
+ - `S3_ENDPOINT` — optional, for non-AWS S3.
29
+ - `BUCKET` — bucket to list.
30
+ - `BASE_PATH` — optional prefix; stripped from printed keys.
31
+
32
+ ## Environment variables (gateway mode)
33
+
34
+ - `SERVICE_BASE_URL` — split on `/` to extract `endpoint_url` and
35
+ `bucket`.
36
+ - `UNITYSVC_API_KEY` — used as `aws_access_key_id`.
37
+
38
+ ## Conventions
39
+
40
+ - Ends with `print("connectivity ok")` so the same script is usable as
41
+ a smoke test if a seller wants to reuse it.
42
+ - Prints `<key> (<size> bytes)` per object, up to five.
43
+
44
+ ## Versions
45
+
46
+ ### v1 — initial release
47
+
48
+ - Three-branch (seller creds / unsigned / gateway) rendering based on
49
+ `local_testing` and `interface.access_key`.
@@ -0,0 +1,48 @@
1
+ import boto3
2
+ import os
3
+ {% if local_testing %}
4
+ {% if interface.get("access_key") %}
5
+ # Local testing: access S3 with seller credentials
6
+ s3_kwargs = {
7
+ 'region_name': os.environ['REGION'],
8
+ 'aws_access_key_id': os.environ['ACCESS_KEY'],
9
+ 'aws_secret_access_key': os.environ['SECRET_KEY'],
10
+ }
11
+ # Use S3_ENDPOINT for non-AWS endpoints (DigitalOcean Spaces, MinIO, etc.)
12
+ if os.environ.get('S3_ENDPOINT'):
13
+ s3_kwargs['endpoint_url'] = os.environ['S3_ENDPOINT']
14
+ s3 = boto3.client('s3', **s3_kwargs)
15
+ {% else %}
16
+ from botocore import UNSIGNED
17
+ from botocore.config import Config
18
+
19
+ # Local testing: access public S3 bucket directly (no authentication needed)
20
+ s3 = boto3.client('s3',
21
+ region_name=os.environ['REGION'],
22
+ config=Config(signature_version=UNSIGNED),
23
+ )
24
+ {% endif %}
25
+ bucket = os.environ['BUCKET']
26
+ prefix = os.environ.get('BASE_PATH', '')
27
+ {% else %}
28
+ # Access via UnitySVC S3 gateway
29
+ service_url = os.environ['SERVICE_BASE_URL']
30
+ endpoint_url = service_url.rsplit('/', 1)[0]
31
+ bucket = service_url.rsplit('/', 1)[1]
32
+ prefix = ''
33
+
34
+ s3 = boto3.client('s3',
35
+ endpoint_url=endpoint_url,
36
+ aws_access_key_id=os.environ['UNITYSVC_API_KEY'],
37
+ aws_secret_access_key='not-used',
38
+ )
39
+ {% endif %}
40
+
41
+ # List files in the bucket
42
+ response = s3.list_objects_v2(Bucket=bucket, MaxKeys=5, Prefix=prefix)
43
+ for obj in response.get('Contents', []):
44
+ key = obj['Key']
45
+ if prefix and key.startswith(prefix):
46
+ key = key[len(prefix):]
47
+ print(f"{key} ({obj['Size']} bytes)")
48
+ print("connectivity ok")