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.
- unitysvc_data/__init__.py +119 -0
- unitysvc_data/_manifest.json +91 -0
- unitysvc_data/_version.py +1 -0
- unitysvc_data/cli.py +222 -0
- unitysvc_data/examples/api/connectivity/README.md +45 -0
- unitysvc_data/examples/api/connectivity/connectivity-v1.sh.j2 +42 -0
- unitysvc_data/examples/llm/request-template/README.md +50 -0
- unitysvc_data/examples/llm/request-template/request-template-v1.json +13 -0
- unitysvc_data/examples/s3/code-example/README.md +49 -0
- unitysvc_data/examples/s3/code-example/code-example-v1.py.j2 +48 -0
- unitysvc_data/examples/s3/connectivity/README.md +53 -0
- unitysvc_data/examples/s3/connectivity/connectivity-v1.py.j2 +63 -0
- unitysvc_data/examples/s3/description/README.md +45 -0
- unitysvc_data/examples/s3/description/description-v1.md +21 -0
- unitysvc_data/examples/smtp/connectivity/README.md +50 -0
- unitysvc_data/examples/smtp/connectivity/connectivity-v1.sh.j2 +34 -0
- unitysvc_data/presets.py +259 -0
- unitysvc_data/py.typed +0 -0
- unitysvc_data-0.1.0.dist-info/METADATA +189 -0
- unitysvc_data-0.1.0.dist-info/RECORD +24 -0
- unitysvc_data-0.1.0.dist-info/WHEEL +5 -0
- unitysvc_data-0.1.0.dist-info/entry_points.txt +2 -0
- unitysvc_data-0.1.0.dist-info/licenses/LICENSE +21 -0
- unitysvc_data-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
+++
|
|
2
|
+
preset_name = "s3_connectivity"
|
|
3
|
+
category = "connectivity_test"
|
|
4
|
+
mime_type = "python"
|
|
5
|
+
file = "connectivity.py.j2"
|
|
6
|
+
description = "Verify S3 endpoint accepts the configured credentials"
|
|
7
|
+
is_active = true
|
|
8
|
+
is_public = false
|
|
9
|
+
meta = { output_contains = "connectivity ok" }
|
|
10
|
+
+++
|
|
11
|
+
|
|
12
|
+
# s3 / connectivity — S3 credential smoke test
|
|
13
|
+
|
|
14
|
+
Python smoke test that proves **credentials are accepted** by the S3
|
|
15
|
+
endpoint. It does not try to access a specific bucket — the point is
|
|
16
|
+
"can the user authenticate against the upstream", not "does bucket X
|
|
17
|
+
exist" — so AccessDenied on a list call still counts as pass.
|
|
18
|
+
|
|
19
|
+
## Branches (rendered at upload time with the listing context)
|
|
20
|
+
|
|
21
|
+
| `local_testing` | `interface.access_key` | Behaviour |
|
|
22
|
+
|-----------------|------------------------|-----------|
|
|
23
|
+
| `true` | present | Signs with seller credentials and calls `list_buckets()`. |
|
|
24
|
+
| `true` | absent | Public bucket — prints `connectivity ok` and exits 0 without network call. |
|
|
25
|
+
| `false` | — | Signs with the customer's `UNITYSVC_API_KEY` via the gateway and calls `list_objects_v2(Bucket=..., MaxKeys=1)` against the gateway-resolved bucket. |
|
|
26
|
+
|
|
27
|
+
## Environment variables (local mode)
|
|
28
|
+
|
|
29
|
+
- `REGION`, `ACCESS_KEY`, `SECRET_KEY` — seller credentials.
|
|
30
|
+
- `S3_ENDPOINT` — optional; set for non-AWS endpoints (DigitalOcean
|
|
31
|
+
Spaces, MinIO, etc.).
|
|
32
|
+
|
|
33
|
+
## Environment variables (gateway mode)
|
|
34
|
+
|
|
35
|
+
- `SERVICE_BASE_URL` — form: `{endpoint_prefix}/{service_slug}`. The
|
|
36
|
+
script splits on the last `/` to recover `endpoint_url` and `bucket`.
|
|
37
|
+
- `UNITYSVC_API_KEY` — customer's gateway key, used as
|
|
38
|
+
`aws_access_key_id`; `aws_secret_access_key` is set to `"not-used"`
|
|
39
|
+
because the gateway ignores it.
|
|
40
|
+
|
|
41
|
+
## Auth-failure classification
|
|
42
|
+
|
|
43
|
+
Only these error codes are treated as genuine auth rejection:
|
|
44
|
+
`InvalidAccessKeyId`, `SignatureDoesNotMatch`, `InvalidClientTokenId`,
|
|
45
|
+
and any HTTP 401. Anything else (including 403 AccessDenied) means the
|
|
46
|
+
signature was accepted — that's still connectivity ok.
|
|
47
|
+
|
|
48
|
+
## Versions
|
|
49
|
+
|
|
50
|
+
### v1 — initial release
|
|
51
|
+
|
|
52
|
+
- Three-way branch on `local_testing` / `interface.access_key`.
|
|
53
|
+
- Treats non-auth `ClientError` as pass.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
5
|
+
{% if local_testing %}
|
|
6
|
+
{% if interface.get("access_key") %}
|
|
7
|
+
# Local testing: verify credentials are accepted by the S3 endpoint.
|
|
8
|
+
# We intentionally do NOT target a specific bucket — this test is about
|
|
9
|
+
# "can the user authenticate against the upstream", not "does bucket X
|
|
10
|
+
# exist". list_buckets is the cheapest signed call: credentials are
|
|
11
|
+
# validated regardless of whether the account is actually allowed to
|
|
12
|
+
# enumerate buckets (AccessDenied on the list still proves auth worked).
|
|
13
|
+
s3_kwargs = {
|
|
14
|
+
'region_name': os.environ['REGION'],
|
|
15
|
+
'aws_access_key_id': os.environ['ACCESS_KEY'],
|
|
16
|
+
'aws_secret_access_key': os.environ['SECRET_KEY'],
|
|
17
|
+
}
|
|
18
|
+
if os.environ.get('S3_ENDPOINT'):
|
|
19
|
+
s3_kwargs['endpoint_url'] = os.environ['S3_ENDPOINT']
|
|
20
|
+
s3 = boto3.client('s3', **s3_kwargs)
|
|
21
|
+
{% else %}
|
|
22
|
+
# Public bucket: no credentials to verify, nothing to check.
|
|
23
|
+
print("connectivity ok (public bucket — no credentials to verify)")
|
|
24
|
+
sys.exit(0)
|
|
25
|
+
{% endif %}
|
|
26
|
+
{% else %}
|
|
27
|
+
# Access via UnitySVC S3 gateway — sign with the customer's API key.
|
|
28
|
+
# SERVICE_BASE_URL is {endpoint_prefix}/{service_slug} (e.g. http://host:9180/demo/s3).
|
|
29
|
+
# Split on the last '/' so boto3 path-style addressing reconstructs the full URL.
|
|
30
|
+
service_url = os.environ['SERVICE_BASE_URL'].rstrip('/')
|
|
31
|
+
endpoint_url = service_url.rsplit('/', 1)[0]
|
|
32
|
+
bucket = service_url.rsplit('/', 1)[1]
|
|
33
|
+
s3 = boto3.client('s3',
|
|
34
|
+
endpoint_url=endpoint_url,
|
|
35
|
+
aws_access_key_id=os.environ['UNITYSVC_API_KEY'],
|
|
36
|
+
aws_secret_access_key='not-used',
|
|
37
|
+
region_name='us-east-1',
|
|
38
|
+
)
|
|
39
|
+
{% endif %}
|
|
40
|
+
|
|
41
|
+
# Auth-error codes: credentials were rejected. Anything else (including
|
|
42
|
+
# 403 AccessDenied on list_buckets) still proves the signature was
|
|
43
|
+
# accepted — the caller just isn't allowed to list. That counts as ok.
|
|
44
|
+
AUTH_FAILURES = {'InvalidAccessKeyId', 'SignatureDoesNotMatch', 'InvalidClientTokenId'}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
{% if local_testing %}
|
|
48
|
+
s3.list_buckets()
|
|
49
|
+
{% else %}
|
|
50
|
+
s3.list_objects_v2(Bucket=bucket, MaxKeys=1)
|
|
51
|
+
{% endif %}
|
|
52
|
+
except ClientError as e:
|
|
53
|
+
status = e.response.get('ResponseMetadata', {}).get('HTTPStatusCode')
|
|
54
|
+
code = e.response.get('Error', {}).get('Code', '')
|
|
55
|
+
if status == 401 or code in AUTH_FAILURES:
|
|
56
|
+
print(f"connectivity failed: credentials rejected ({code or status})", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
# Non-auth errors: credentials were accepted, endpoint just didn't
|
|
59
|
+
# like the specific call. That's still connectivity ok.
|
|
60
|
+
except BotoCoreError as e:
|
|
61
|
+
print(f"connectivity failed: {e}", file=sys.stderr)
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
print("connectivity ok")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
+++
|
|
2
|
+
preset_name = "s3_description"
|
|
3
|
+
category = "description"
|
|
4
|
+
mime_type = "markdown"
|
|
5
|
+
file = "description.md"
|
|
6
|
+
description = "Customer-facing overview of the S3 gateway service"
|
|
7
|
+
is_active = true
|
|
8
|
+
is_public = true
|
|
9
|
+
+++
|
|
10
|
+
|
|
11
|
+
# s3 / description — S3 gateway service description
|
|
12
|
+
|
|
13
|
+
Markdown overview shown to customers on the listing page. Gives them a
|
|
14
|
+
short narrative about the gateway plus a minimal `boto3` snippet they
|
|
15
|
+
can copy-paste.
|
|
16
|
+
|
|
17
|
+
## What's in the description
|
|
18
|
+
|
|
19
|
+
- One-sentence framing of the gateway.
|
|
20
|
+
- A `boto3.client('s3', ...)` snippet with the four fields a customer
|
|
21
|
+
needs to fill in: `endpoint_url`, `aws_access_key_id`,
|
|
22
|
+
`aws_secret_access_key` (always `"not-used"` for this gateway), and
|
|
23
|
+
the `Bucket` for the `list_objects_v2` call.
|
|
24
|
+
- Closing paragraph explaining how the gateway authenticates, resolves
|
|
25
|
+
the upstream bucket, and proxies the request.
|
|
26
|
+
|
|
27
|
+
## Template tokens — **not** Jinja2
|
|
28
|
+
|
|
29
|
+
The description file contains `{{ S3_GATEWAY_PUBLIC_URL }}`,
|
|
30
|
+
`{{ API_KEY }}`, and `{{ SERVICE_NAME }}` tokens. These are deliberately
|
|
31
|
+
**not** rendered by the SDK — the file's name is `description.md`, not
|
|
32
|
+
`description.md.j2`, so Pass-2 rendering leaves the tokens intact.
|
|
33
|
+
Customers see them as literal placeholders they're expected to replace
|
|
34
|
+
with their own values.
|
|
35
|
+
|
|
36
|
+
If a future variant wants server-rendered tokens, add it as a new
|
|
37
|
+
`[[versions]]` entry with a `description-v2.md.j2` file alongside the
|
|
38
|
+
existing `description-v1.md` — do not convert `_v1` in place.
|
|
39
|
+
|
|
40
|
+
## Versions
|
|
41
|
+
|
|
42
|
+
### v1 — initial release
|
|
43
|
+
|
|
44
|
+
- Static markdown; `{{ ... }}` tokens are literal placeholders shown
|
|
45
|
+
to customers.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
## S3 Content via UnitySVC Storage Gateway
|
|
2
|
+
|
|
3
|
+
Access S3-compatible content through UnitySVC's storage gateway using any
|
|
4
|
+
S3 client (boto3, aws-cli, rclone, cyberduck) authenticated with your
|
|
5
|
+
UnitySVC API key.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import boto3
|
|
9
|
+
|
|
10
|
+
s3 = boto3.client('s3',
|
|
11
|
+
endpoint_url='{{ S3_GATEWAY_PUBLIC_URL }}',
|
|
12
|
+
aws_access_key_id='{{ API_KEY }}',
|
|
13
|
+
aws_secret_access_key='not-used',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Browse available files
|
|
17
|
+
s3.list_objects_v2(Bucket='{{ SERVICE_NAME }}', MaxKeys=10)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The gateway authenticates you against your UnitySVC API key, resolves the
|
|
21
|
+
upstream bucket from the service configuration, and proxies the request.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
+++
|
|
2
|
+
preset_name = "smtp_connectivity"
|
|
3
|
+
category = "connectivity_test"
|
|
4
|
+
mime_type = "bash"
|
|
5
|
+
file = "connectivity.sh.j2"
|
|
6
|
+
description = "Verify SMTP server returns a 220 greeting on connect"
|
|
7
|
+
is_active = true
|
|
8
|
+
is_public = false
|
|
9
|
+
meta = { output_contains = "connectivity ok" }
|
|
10
|
+
+++
|
|
11
|
+
|
|
12
|
+
# smtp / connectivity — SMTP banner smoke test
|
|
13
|
+
|
|
14
|
+
Bash smoke test for mail-submission services routed through the
|
|
15
|
+
UnitySVC SMTP relay. A healthy server answers a TCP connect with
|
|
16
|
+
`220 ... ESMTP ...` within five seconds; anything else is failure.
|
|
17
|
+
|
|
18
|
+
## Modes
|
|
19
|
+
|
|
20
|
+
- **Local** — harness sets `$HOST` and `$PORT` from
|
|
21
|
+
`upstream_access_config`. `$PORT` defaults to 587 (submission).
|
|
22
|
+
- **Gateway** — harness sets `$SERVICE_BASE_URL` as `smtp://host:port`
|
|
23
|
+
or `smtps://host:port`. The script strips the scheme, splits host
|
|
24
|
+
from port, and defaults the port to 587 if absent.
|
|
25
|
+
|
|
26
|
+
The script picks the mode by looking at which env var is set — no
|
|
27
|
+
`local_testing` conditional needed.
|
|
28
|
+
|
|
29
|
+
## How the test works
|
|
30
|
+
|
|
31
|
+
1. Open a TCP connection via `nc -w 5` (5-second cap on both connect
|
|
32
|
+
and idle-after-banner).
|
|
33
|
+
2. Send `QUIT` so the server closes cleanly instead of timing out on
|
|
34
|
+
us.
|
|
35
|
+
3. Read the first line of output.
|
|
36
|
+
4. Accept any line starting with `220 `.
|
|
37
|
+
|
|
38
|
+
## Conventions
|
|
39
|
+
|
|
40
|
+
- Success line: `connectivity ok (220 ...)` — `meta.output_contains`
|
|
41
|
+
uses `connectivity ok` so any banner text passes.
|
|
42
|
+
- Does not run `STARTTLS` or authenticate. This test is strictly
|
|
43
|
+
"pipe opens and server says it's SMTP".
|
|
44
|
+
- Ports 25 and 465 work too; override `PORT` in the env.
|
|
45
|
+
|
|
46
|
+
## Versions
|
|
47
|
+
|
|
48
|
+
### v1 — initial release
|
|
49
|
+
|
|
50
|
+
- Uses `nc -w 5`; sends `QUIT`; accepts any `220 ...` banner.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Connectivity test for SMTP services. A healthy mail server sends a
|
|
3
|
+
# "220 ... ESMTP ..." greeting on connect; anything else (no response,
|
|
4
|
+
# timeout, or other greeting code) is treated as failure.
|
|
5
|
+
#
|
|
6
|
+
# Env var layout the harness provides:
|
|
7
|
+
# local mode — $HOST, $PORT (from upstream_access_config)
|
|
8
|
+
# online mode — $SERVICE_BASE_URL (smtp://host:port or smtps://host:port)
|
|
9
|
+
# We branch on whichever is present so the same script works in both.
|
|
10
|
+
set -o pipefail
|
|
11
|
+
|
|
12
|
+
if [ -n "${HOST:-}" ]; then
|
|
13
|
+
PORT="${PORT:-587}"
|
|
14
|
+
else
|
|
15
|
+
URL="${SERVICE_BASE_URL:?need HOST or SERVICE_BASE_URL}"
|
|
16
|
+
stripped="${URL#smtp://}"
|
|
17
|
+
stripped="${stripped#smtps://}"
|
|
18
|
+
HOST="${stripped%%:*}"
|
|
19
|
+
PORT="${stripped#*:}"
|
|
20
|
+
PORT="${PORT%%/*}"
|
|
21
|
+
PORT="${PORT:-587}"
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# `nc -w 5` caps both connect wait and idle-after-banner wait. Send QUIT
|
|
25
|
+
# so the server closes cleanly instead of timing out on us.
|
|
26
|
+
greeting=$( (echo "QUIT"; sleep 1) | nc -w 5 "$HOST" "$PORT" 2>/dev/null | head -1 )
|
|
27
|
+
|
|
28
|
+
if [[ "$greeting" == "220 "* ]]; then
|
|
29
|
+
echo "connectivity ok ($greeting)"
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
echo "connectivity failed: expected '220 ...' greeting from ${HOST}:${PORT}, got: ${greeting:-<no response>}" >&2
|
|
34
|
+
exit 1
|
unitysvc_data/presets.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Preset document records and file content, backed by bundled example files.
|
|
2
|
+
|
|
3
|
+
Loaded from ``_manifest.json`` at import time so installs don't pay for
|
|
4
|
+
a filesystem walk. The manifest is generated by ``tools/build.py`` from
|
|
5
|
+
the per-family ``README.md`` front-matter; CI validates it's up to
|
|
6
|
+
date on every PR.
|
|
7
|
+
|
|
8
|
+
Two primitives are exposed:
|
|
9
|
+
|
|
10
|
+
- :func:`doc_preset` — returns a fully-populated document record
|
|
11
|
+
(``category``, ``description``, ``mime_type``, ``file_path``, ...),
|
|
12
|
+
suitable for dropping into a seller's ``listing.json`` ``documents``
|
|
13
|
+
map. This is the expansion the SDK applies to ``$preset`` JSON
|
|
14
|
+
sentinels at upload time.
|
|
15
|
+
|
|
16
|
+
- :func:`file_preset` — returns the raw UTF-8 content of the bundled
|
|
17
|
+
example file itself. Useful when the example isn't a listing
|
|
18
|
+
document — for embedding as a code snippet, feeding to a test
|
|
19
|
+
harness, or any future use that needs the bytes rather than the
|
|
20
|
+
document metadata.
|
|
21
|
+
|
|
22
|
+
Both accept either a bare preset name (``"s3_connectivity_v1"``,
|
|
23
|
+
``"s3_connectivity"``) or a ``{"$preset": ..., "$with": ...}`` sentinel,
|
|
24
|
+
so the same function works from JSON walkers and from programmatic
|
|
25
|
+
Python code. Version-less aliases (``s3_connectivity``) resolve to the
|
|
26
|
+
latest published version in the family.
|
|
27
|
+
|
|
28
|
+
See https://github.com/unitysvc/unitysvc-sellers/issues/25 for design.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
from copy import deepcopy
|
|
35
|
+
from importlib.resources import files as _files
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
# Resolved once at import time. Duplicated with __init__ rather than
|
|
40
|
+
# imported to avoid a circular import during package load.
|
|
41
|
+
_EXAMPLES_ROOT = _files(__name__).joinpath("examples")
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"PRESETS",
|
|
45
|
+
"MANIFEST",
|
|
46
|
+
"ALIASES",
|
|
47
|
+
"OVERRIDABLE",
|
|
48
|
+
"doc_preset",
|
|
49
|
+
"file_preset",
|
|
50
|
+
"list_presets",
|
|
51
|
+
"register_jinja_globals",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# Fields a seller may override when expanding a preset. Everything else
|
|
55
|
+
# — category, mime_type, file_path — is tied to the bundled file
|
|
56
|
+
# content and rejected as an override.
|
|
57
|
+
OVERRIDABLE: frozenset[str] = frozenset({"description", "is_public", "is_active", "meta"})
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _load_manifest() -> dict[str, Any]:
|
|
61
|
+
manifest_bytes = _files(__name__).joinpath("_manifest.json").read_bytes()
|
|
62
|
+
return json.loads(manifest_bytes)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _resolve_example_path(relative_file: str) -> str:
|
|
66
|
+
target = _EXAMPLES_ROOT.joinpath(relative_file)
|
|
67
|
+
if not target.is_file():
|
|
68
|
+
raise FileNotFoundError(
|
|
69
|
+
f"Manifest references missing file {relative_file!r}. "
|
|
70
|
+
f"Rebuild with `python tools/build.py`."
|
|
71
|
+
)
|
|
72
|
+
return str(Path(str(target)))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
MANIFEST: dict[str, Any] = _load_manifest()
|
|
76
|
+
_PRESET_RECORDS: dict[str, dict[str, Any]] = {}
|
|
77
|
+
|
|
78
|
+
for _name, _entry in MANIFEST["presets"].items():
|
|
79
|
+
_PRESET_RECORDS[_name] = {
|
|
80
|
+
"category": _entry["category"],
|
|
81
|
+
"description": _entry["description"],
|
|
82
|
+
"file_path": _resolve_example_path(_entry["example_file"]),
|
|
83
|
+
"is_active": _entry["is_active"],
|
|
84
|
+
"is_public": _entry["is_public"],
|
|
85
|
+
"meta": dict(_entry.get("meta", {})),
|
|
86
|
+
"mime_type": _entry["mime_type"],
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Alias entries share the same underlying record as their target. We
|
|
90
|
+
# store the record under both names so a lookup of either resolves in
|
|
91
|
+
# O(1) without chasing pointers.
|
|
92
|
+
ALIASES: dict[str, str] = dict(MANIFEST.get("aliases", {}))
|
|
93
|
+
for _alias, _target in ALIASES.items():
|
|
94
|
+
if _target not in _PRESET_RECORDS:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
f"_manifest.json alias {_alias!r} points at missing preset {_target!r}. "
|
|
97
|
+
f"Rebuild with `python tools/build.py`."
|
|
98
|
+
)
|
|
99
|
+
_PRESET_RECORDS[_alias] = _PRESET_RECORDS[_target]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _make_factory(record: dict[str, Any]):
|
|
103
|
+
def _factory(**overrides: Any) -> dict[str, Any]:
|
|
104
|
+
bad = set(overrides) - OVERRIDABLE
|
|
105
|
+
if bad:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Cannot override {sorted(bad)!r} — these fields are tied to the file content. "
|
|
108
|
+
f"Allowed overrides: {sorted(OVERRIDABLE)!r}"
|
|
109
|
+
)
|
|
110
|
+
out = deepcopy(record)
|
|
111
|
+
for key, value in overrides.items():
|
|
112
|
+
if key == "meta" and isinstance(value, dict) and isinstance(out.get("meta"), dict):
|
|
113
|
+
out["meta"] = {**out["meta"], **value}
|
|
114
|
+
else:
|
|
115
|
+
out[key] = value
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
return _factory
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
PRESETS: dict[str, Any] = {name: _make_factory(record) for name, record in _PRESET_RECORDS.items()}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --- Source parsing --------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _parse_source(source: Any) -> tuple[str, dict[str, Any]]:
|
|
128
|
+
"""Normalise a preset reference to ``(name, overrides)``.
|
|
129
|
+
|
|
130
|
+
Accepts either a bare name string or a ``{"$preset": ..., "$with": ...}``
|
|
131
|
+
sentinel dict. Returns ``(name, {})`` for string sources since
|
|
132
|
+
bare-name callers supply overrides as kwargs instead.
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(source, str):
|
|
135
|
+
return source, {}
|
|
136
|
+
|
|
137
|
+
if not isinstance(source, dict):
|
|
138
|
+
raise TypeError(
|
|
139
|
+
f"Expected a preset name string or a $preset sentinel dict, "
|
|
140
|
+
f"got {type(source).__name__}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if "$preset" not in source:
|
|
144
|
+
raise ValueError(f"Node is not a preset sentinel (missing '$preset'): {source!r}")
|
|
145
|
+
|
|
146
|
+
unknown = set(source) - {"$preset", "$with"}
|
|
147
|
+
if unknown:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Unknown keys in preset sentinel: {sorted(unknown)!r}. "
|
|
150
|
+
f"Only '$preset' and '$with' are allowed."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
name = source["$preset"]
|
|
154
|
+
if not isinstance(name, str):
|
|
155
|
+
raise ValueError(f"'$preset' must be a string, got {type(name).__name__}")
|
|
156
|
+
|
|
157
|
+
overrides = source.get("$with") or {}
|
|
158
|
+
if not isinstance(overrides, dict):
|
|
159
|
+
raise ValueError(f"'$with' must be an object, got {type(overrides).__name__}")
|
|
160
|
+
|
|
161
|
+
return name, overrides
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _lookup(name: str, pool: dict[str, Any], what: str) -> Any:
|
|
165
|
+
try:
|
|
166
|
+
return pool[name]
|
|
167
|
+
except KeyError:
|
|
168
|
+
raise KeyError(
|
|
169
|
+
f"Unknown preset: {name!r}. Available {what}s: {sorted(pool)!r}"
|
|
170
|
+
) from None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# --- Public API ------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def doc_preset(source: Any, **overrides: Any) -> dict[str, Any]:
|
|
177
|
+
"""Return a fully-populated document record for the named preset.
|
|
178
|
+
|
|
179
|
+
``source`` may be a bare preset name (``"s3_connectivity_v1"`` or
|
|
180
|
+
the version-less alias ``"s3_connectivity"``) or a JSON sentinel
|
|
181
|
+
``{"$preset": ..., "$with": {...}}`` as it appears in a parsed
|
|
182
|
+
``listing.json``.
|
|
183
|
+
|
|
184
|
+
Overrides (``description``, ``is_active``, ``is_public``, ``meta``)
|
|
185
|
+
may come from ``$with`` when using the sentinel form or from
|
|
186
|
+
keyword arguments when passing a bare name — but not both at once.
|
|
187
|
+
Attempting to override ``category``, ``mime_type``, or ``file_path``
|
|
188
|
+
raises :class:`ValueError` because those are tied to the bundled
|
|
189
|
+
file content.
|
|
190
|
+
|
|
191
|
+
The returned dict is a fresh copy on every call; callers may
|
|
192
|
+
mutate it freely.
|
|
193
|
+
"""
|
|
194
|
+
name, sentinel_overrides = _parse_source(source)
|
|
195
|
+
if sentinel_overrides and overrides:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"Cannot combine keyword overrides with a $preset sentinel's '$with' block — "
|
|
198
|
+
"pick one."
|
|
199
|
+
)
|
|
200
|
+
factory = _lookup(name, PRESETS, "preset")
|
|
201
|
+
return factory(**(sentinel_overrides or overrides))
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def file_preset(source: Any) -> str:
|
|
205
|
+
"""Return the raw UTF-8 content of the preset's bundled file.
|
|
206
|
+
|
|
207
|
+
``source`` may be a bare preset name or a ``$preset`` sentinel.
|
|
208
|
+
Overrides are rejected because file content is immutable — use
|
|
209
|
+
:func:`doc_preset` if you need a document record with per-field
|
|
210
|
+
overrides.
|
|
211
|
+
|
|
212
|
+
.. note::
|
|
213
|
+
|
|
214
|
+
For Jinja2 templates (``.j2`` files), the returned string is
|
|
215
|
+
the **raw template source** — constructs like
|
|
216
|
+
``{% if local_testing %}`` are preserved verbatim. Rendering
|
|
217
|
+
requires listing / interface / provider context that only the
|
|
218
|
+
calling SDK knows; the sellers SDK applies that rendering via
|
|
219
|
+
``render_template_file`` at upload time. If you're piping the
|
|
220
|
+
content to a runnable file (e.g.
|
|
221
|
+
``usvc_data file-preset ... > smoke.py``) and the preset
|
|
222
|
+
targets a ``.j2`` file, you'll get a template, not an
|
|
223
|
+
executable script.
|
|
224
|
+
"""
|
|
225
|
+
name, sentinel_overrides = _parse_source(source)
|
|
226
|
+
if sentinel_overrides:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"file_preset does not support '$with' overrides — the file content is immutable. "
|
|
229
|
+
"Use doc_preset() if you need a document record with per-field overrides."
|
|
230
|
+
)
|
|
231
|
+
record = _lookup(name, _PRESET_RECORDS, "preset")
|
|
232
|
+
return Path(record["file_path"]).read_text(encoding="utf-8")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def list_presets() -> tuple[list[str], dict[str, str]]:
|
|
236
|
+
"""Return ``(versioned_names, aliases)``.
|
|
237
|
+
|
|
238
|
+
*versioned_names* is the sorted list of concrete preset names
|
|
239
|
+
(``s3_connectivity_v1``, ...); *aliases* maps each version-less
|
|
240
|
+
family name to its current latest versioned name
|
|
241
|
+
(``s3_connectivity`` → ``s3_connectivity_v1``).
|
|
242
|
+
|
|
243
|
+
Symmetric with ``usvc_data list`` on the CLI.
|
|
244
|
+
"""
|
|
245
|
+
return sorted(MANIFEST["presets"]), dict(ALIASES)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def register_jinja_globals(env: Any) -> None:
|
|
249
|
+
"""Register every preset factory as a Jinja2 global on *env*.
|
|
250
|
+
|
|
251
|
+
Lets generated-repo templates write ``{{ s3_connectivity_v1() | tojson }}``
|
|
252
|
+
directly in ``listing.json.j2`` instead of the ``$preset`` JSON
|
|
253
|
+
sentinel. Hand-authored repos use the sentinel path and never need
|
|
254
|
+
this hook.
|
|
255
|
+
|
|
256
|
+
Both concrete names (``s3_connectivity_v1``) and aliases
|
|
257
|
+
(``s3_connectivity``) are registered.
|
|
258
|
+
"""
|
|
259
|
+
env.globals.update(PRESETS)
|
unitysvc_data/py.typed
ADDED
|
File without changes
|