ausca 0.0.2__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.
- ausca-0.0.2/PKG-INFO +96 -0
- ausca-0.0.2/README.md +80 -0
- ausca-0.0.2/pyproject.toml +29 -0
- ausca-0.0.2/setup.cfg +4 -0
- ausca-0.0.2/src/ausca/__init__.py +47 -0
- ausca-0.0.2/src/ausca/artifacts.py +97 -0
- ausca-0.0.2/src/ausca/cli.py +181 -0
- ausca-0.0.2/src/ausca/client.py +244 -0
- ausca-0.0.2/src/ausca/payment.py +129 -0
- ausca-0.0.2/src/ausca.egg-info/PKG-INFO +96 -0
- ausca-0.0.2/src/ausca.egg-info/SOURCES.txt +15 -0
- ausca-0.0.2/src/ausca.egg-info/dependency_links.txt +1 -0
- ausca-0.0.2/src/ausca.egg-info/entry_points.txt +2 -0
- ausca-0.0.2/src/ausca.egg-info/requires.txt +3 -0
- ausca-0.0.2/src/ausca.egg-info/top_level.txt +1 -0
- ausca-0.0.2/tests/test_client.py +148 -0
- ausca-0.0.2/tests/test_ports.py +206 -0
ausca-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ausca
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Client for Ausca metered agent infrastructure services: catalog-bound invocations paid per call under a hard USD cap, through pluggable payment authorities (x402 v2 today), with receipt-backed results.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://ausca.com
|
|
7
|
+
Project-URL: Documentation, https://ausca.com/docs
|
|
8
|
+
Project-URL: Repository, https://github.com/auscahq/ausca-integrations
|
|
9
|
+
Project-URL: Issues, https://github.com/auscahq/ausca-integrations/issues
|
|
10
|
+
Keywords: ausca,x402,agents,payments,mcp
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: httpx>=0.28
|
|
14
|
+
Requires-Dist: x402[evm]>=2.21.0
|
|
15
|
+
Requires-Dist: eth-account>=0.13
|
|
16
|
+
|
|
17
|
+
# ausca
|
|
18
|
+
|
|
19
|
+
Client for [Ausca](https://ausca.com) metered agent infrastructure services:
|
|
20
|
+
document OCR, document analysis, media transcription, browser sessions, and
|
|
21
|
+
agent inboxes. Paid per call under a hard USD cap you set, with no account
|
|
22
|
+
or provider API keys, and a settlement receipt on every completed
|
|
23
|
+
invocation.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install ausca
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import os
|
|
35
|
+
from ausca import AuscaClient
|
|
36
|
+
|
|
37
|
+
client = AuscaClient.with_local_key(
|
|
38
|
+
private_key=os.environ["AUSCA_PRIVATE_KEY"], # pays USDC on Base
|
|
39
|
+
max_payment_usd=0.50, # hard per-call cap
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
outcome = client.invoke("browser.session", {"duration_seconds": 600})
|
|
43
|
+
print(outcome.result)
|
|
44
|
+
print(outcome.payment.transaction) # settlement proof
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`invoke` resolves the offer's immutable revision, schema digests, and
|
|
48
|
+
payable route from the live catalog, reads the exact payment requirement,
|
|
49
|
+
pays it if it fits the cap, and retries the same bytes with the same
|
|
50
|
+
idempotency key. Each call starts with a fresh key. For recovery after an
|
|
51
|
+
uncertain response, pass the same caller-owned `idempotency_key`; use a new
|
|
52
|
+
key for another intentional purchase, even when its input is identical.
|
|
53
|
+
|
|
54
|
+
A CLI ships with the package:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ausca catalog
|
|
58
|
+
ausca price document.ocr
|
|
59
|
+
AUSCA_PRIVATE_KEY=0x... AUSCA_MAX_PAYMENT_USD=0.50 \
|
|
60
|
+
ausca invoke browser.session '{"duration_seconds":600}'
|
|
61
|
+
# Reuse this key only to recover that same intended purchase:
|
|
62
|
+
ausca invoke browser.session '{"duration_seconds":600}' \
|
|
63
|
+
--idempotency-key browser-attempt-20260903-0001
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Payment rails
|
|
67
|
+
|
|
68
|
+
Payment is a pluggable authority, never a client concern. The built-in
|
|
69
|
+
`LocalKeyAuthority` pays x402 v2 with a local signing key through the
|
|
70
|
+
official [`x402`](https://pypi.org/project/x402/) library, which enforces
|
|
71
|
+
the cap through its spend controls before anything is signed; other rails
|
|
72
|
+
implement the same small `PaymentAuthority` protocol.
|
|
73
|
+
`client.pay_request(url, ...)` runs one paid call against any resource the
|
|
74
|
+
configured authority can pay; nothing in it is vendor-specific.
|
|
75
|
+
|
|
76
|
+
## Artifact-backed offers
|
|
77
|
+
|
|
78
|
+
Document and media offers take an immutable artifact commitment instead of
|
|
79
|
+
raw bytes. `ausca commit file.pdf` sends the bytes to Ausca's keyless
|
|
80
|
+
temporary ingress and returns the commitment the offer input carries; the
|
|
81
|
+
library equivalent is `client.commit(data, media_type)`. Repeating the same
|
|
82
|
+
bytes is idempotent. The active offer catalog sets the usable input limit.
|
|
83
|
+
|
|
84
|
+
Successful paid state includes `receipt_ref.public_url`, an immutable
|
|
85
|
+
hash-only proof of the Ausca service, public price, completion time, and
|
|
86
|
+
receipt digest. It contains no request or result bytes, content digests, or
|
|
87
|
+
access capabilities. Anyone holding the unguessable URL can read it.
|
|
88
|
+
|
|
89
|
+
The full agent contract lives at <https://ausca.com/SKILL.md>; discover the
|
|
90
|
+
active offers at <https://ausca.com/catalog.json>. The npm equivalent is
|
|
91
|
+
[`ausca`](https://www.npmjs.com/package/ausca), which adds a local MCP
|
|
92
|
+
server (`npx ausca mcp`).
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
ausca-0.0.2/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# ausca
|
|
2
|
+
|
|
3
|
+
Client for [Ausca](https://ausca.com) metered agent infrastructure services:
|
|
4
|
+
document OCR, document analysis, media transcription, browser sessions, and
|
|
5
|
+
agent inboxes. Paid per call under a hard USD cap you set, with no account
|
|
6
|
+
or provider API keys, and a settlement receipt on every completed
|
|
7
|
+
invocation.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ausca
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
from ausca import AuscaClient
|
|
20
|
+
|
|
21
|
+
client = AuscaClient.with_local_key(
|
|
22
|
+
private_key=os.environ["AUSCA_PRIVATE_KEY"], # pays USDC on Base
|
|
23
|
+
max_payment_usd=0.50, # hard per-call cap
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
outcome = client.invoke("browser.session", {"duration_seconds": 600})
|
|
27
|
+
print(outcome.result)
|
|
28
|
+
print(outcome.payment.transaction) # settlement proof
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`invoke` resolves the offer's immutable revision, schema digests, and
|
|
32
|
+
payable route from the live catalog, reads the exact payment requirement,
|
|
33
|
+
pays it if it fits the cap, and retries the same bytes with the same
|
|
34
|
+
idempotency key. Each call starts with a fresh key. For recovery after an
|
|
35
|
+
uncertain response, pass the same caller-owned `idempotency_key`; use a new
|
|
36
|
+
key for another intentional purchase, even when its input is identical.
|
|
37
|
+
|
|
38
|
+
A CLI ships with the package:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ausca catalog
|
|
42
|
+
ausca price document.ocr
|
|
43
|
+
AUSCA_PRIVATE_KEY=0x... AUSCA_MAX_PAYMENT_USD=0.50 \
|
|
44
|
+
ausca invoke browser.session '{"duration_seconds":600}'
|
|
45
|
+
# Reuse this key only to recover that same intended purchase:
|
|
46
|
+
ausca invoke browser.session '{"duration_seconds":600}' \
|
|
47
|
+
--idempotency-key browser-attempt-20260903-0001
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Payment rails
|
|
51
|
+
|
|
52
|
+
Payment is a pluggable authority, never a client concern. The built-in
|
|
53
|
+
`LocalKeyAuthority` pays x402 v2 with a local signing key through the
|
|
54
|
+
official [`x402`](https://pypi.org/project/x402/) library, which enforces
|
|
55
|
+
the cap through its spend controls before anything is signed; other rails
|
|
56
|
+
implement the same small `PaymentAuthority` protocol.
|
|
57
|
+
`client.pay_request(url, ...)` runs one paid call against any resource the
|
|
58
|
+
configured authority can pay; nothing in it is vendor-specific.
|
|
59
|
+
|
|
60
|
+
## Artifact-backed offers
|
|
61
|
+
|
|
62
|
+
Document and media offers take an immutable artifact commitment instead of
|
|
63
|
+
raw bytes. `ausca commit file.pdf` sends the bytes to Ausca's keyless
|
|
64
|
+
temporary ingress and returns the commitment the offer input carries; the
|
|
65
|
+
library equivalent is `client.commit(data, media_type)`. Repeating the same
|
|
66
|
+
bytes is idempotent. The active offer catalog sets the usable input limit.
|
|
67
|
+
|
|
68
|
+
Successful paid state includes `receipt_ref.public_url`, an immutable
|
|
69
|
+
hash-only proof of the Ausca service, public price, completion time, and
|
|
70
|
+
receipt digest. It contains no request or result bytes, content digests, or
|
|
71
|
+
access capabilities. Anyone holding the unguessable URL can read it.
|
|
72
|
+
|
|
73
|
+
The full agent contract lives at <https://ausca.com/SKILL.md>; discover the
|
|
74
|
+
active offers at <https://ausca.com/catalog.json>. The npm equivalent is
|
|
75
|
+
[`ausca`](https://www.npmjs.com/package/ausca), which adds a local MCP
|
|
76
|
+
server (`npx ausca mcp`).
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ausca"
|
|
7
|
+
version = "0.0.2"
|
|
8
|
+
description = "Client for Ausca metered agent infrastructure services: catalog-bound invocations paid per call under a hard USD cap, through pluggable payment authorities (x402 v2 today), with receipt-backed results."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["ausca", "x402", "agents", "payments", "mcp"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.28",
|
|
15
|
+
"x402[evm]>=2.21.0",
|
|
16
|
+
"eth-account>=0.13",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
ausca = "ausca.cli:main"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://ausca.com"
|
|
24
|
+
Documentation = "https://ausca.com/docs"
|
|
25
|
+
Repository = "https://github.com/auscahq/ausca-integrations"
|
|
26
|
+
Issues = "https://github.com/auscahq/ausca-integrations/issues"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
ausca-0.0.2/setup.cfg
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Ausca: metered agent infrastructure services, paid per call."""
|
|
2
|
+
|
|
3
|
+
from ausca.artifacts import (
|
|
4
|
+
MAX_ARTIFACT_BYTES,
|
|
5
|
+
ArtifactCommitment,
|
|
6
|
+
ArtifactError,
|
|
7
|
+
ArtifactStore,
|
|
8
|
+
AuscaArtifactStore,
|
|
9
|
+
)
|
|
10
|
+
from ausca.client import (
|
|
11
|
+
CATALOG_URL,
|
|
12
|
+
ORIGIN,
|
|
13
|
+
SKILL_URL,
|
|
14
|
+
AuscaClient,
|
|
15
|
+
AuscaError,
|
|
16
|
+
InvocationResult,
|
|
17
|
+
Offer,
|
|
18
|
+
Price,
|
|
19
|
+
PriceOption,
|
|
20
|
+
)
|
|
21
|
+
from ausca.payment import (
|
|
22
|
+
InertAuthority,
|
|
23
|
+
LocalKeyAuthority,
|
|
24
|
+
PaymentAuthority,
|
|
25
|
+
PaymentReceipt,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ArtifactCommitment",
|
|
30
|
+
"ArtifactError",
|
|
31
|
+
"ArtifactStore",
|
|
32
|
+
"AuscaArtifactStore",
|
|
33
|
+
"AuscaClient",
|
|
34
|
+
"AuscaError",
|
|
35
|
+
"CATALOG_URL",
|
|
36
|
+
"InertAuthority",
|
|
37
|
+
"InvocationResult",
|
|
38
|
+
"LocalKeyAuthority",
|
|
39
|
+
"ORIGIN",
|
|
40
|
+
"Offer",
|
|
41
|
+
"PaymentAuthority",
|
|
42
|
+
"PaymentReceipt",
|
|
43
|
+
"Price",
|
|
44
|
+
"PriceOption",
|
|
45
|
+
"MAX_ARTIFACT_BYTES",
|
|
46
|
+
"SKILL_URL",
|
|
47
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Artifact commitments for document and media offers.
|
|
2
|
+
|
|
3
|
+
Artifact-backed offers take an immutable input commitment instead of raw
|
|
4
|
+
bytes. This port turns bytes into that commitment; Ausca's keyless ingress is
|
|
5
|
+
the default implementation and custom stores remain an explicit seam.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import base64
|
|
11
|
+
import hashlib
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Protocol
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
AUSCA_ORIGIN = "https://ausca.com"
|
|
18
|
+
MAX_ARTIFACT_BYTES = 25 * 1024 * 1024
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ArtifactError(Exception):
|
|
22
|
+
"""A refused or failed artifact operation."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ArtifactCommitment:
|
|
27
|
+
"""The immutable input commitment an artifact-backed offer requires."""
|
|
28
|
+
|
|
29
|
+
artifact_ref: str
|
|
30
|
+
content_digest: str
|
|
31
|
+
media_type: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ArtifactStore(Protocol):
|
|
35
|
+
"""Stores bytes and returns the commitment the invocation input carries."""
|
|
36
|
+
|
|
37
|
+
def commit(self, data: bytes, media_type: str) -> ArtifactCommitment: ...
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class AuscaArtifactStore:
|
|
41
|
+
"""Commits bytes through Ausca's keyless temporary artifact ingress."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
*,
|
|
46
|
+
origin: str = AUSCA_ORIGIN,
|
|
47
|
+
http: httpx.Client | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._origin = origin.rstrip("/")
|
|
50
|
+
self._http = http or httpx.Client(timeout=120)
|
|
51
|
+
|
|
52
|
+
def commit(self, data: bytes, media_type: str) -> ArtifactCommitment:
|
|
53
|
+
if not data or len(data) > MAX_ARTIFACT_BYTES:
|
|
54
|
+
raise ArtifactError(f"artifact must contain 1 to {MAX_ARTIFACT_BYTES} bytes")
|
|
55
|
+
if not media_type or media_type.strip() != media_type or len(media_type) > 200:
|
|
56
|
+
raise ArtifactError("artifact media type is invalid")
|
|
57
|
+
digest_hex = hashlib.sha256(data).hexdigest()
|
|
58
|
+
content_digest = f"sha256:{digest_hex}"
|
|
59
|
+
request_hex = hashlib.sha256(
|
|
60
|
+
f"{content_digest}\n{media_type}".encode()
|
|
61
|
+
).hexdigest()
|
|
62
|
+
response = self._http.post(
|
|
63
|
+
f"{self._origin}/v1/artifacts",
|
|
64
|
+
json={
|
|
65
|
+
"data_base64": base64.b64encode(data).decode(),
|
|
66
|
+
"content_digest": content_digest,
|
|
67
|
+
"media_type": media_type,
|
|
68
|
+
"idempotency_key": f"ausca-artifact-{request_hex[:32]}",
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
if response.status_code >= 400:
|
|
72
|
+
raise ArtifactError(
|
|
73
|
+
f"artifact ingress answered {response.status_code}: {response.text[:512]}"
|
|
74
|
+
)
|
|
75
|
+
try:
|
|
76
|
+
result = response.json()
|
|
77
|
+
except ValueError as error:
|
|
78
|
+
raise ArtifactError("artifact ingress answered non-JSON") from error
|
|
79
|
+
if set(result) != {"status", "artifact"} or result["status"] != "stored":
|
|
80
|
+
raise ArtifactError("artifact ingress returned malformed evidence")
|
|
81
|
+
evidence = result["artifact"]
|
|
82
|
+
if not isinstance(evidence, dict) or set(evidence) != {
|
|
83
|
+
"artifact_ref", "content_digest", "created_at", "media_type", "size_bytes"
|
|
84
|
+
}:
|
|
85
|
+
raise ArtifactError("artifact ingress returned malformed evidence")
|
|
86
|
+
artifact_ref = f"runx:artifact:{content_digest}"
|
|
87
|
+
if (
|
|
88
|
+
evidence["artifact_ref"] != artifact_ref
|
|
89
|
+
or evidence["content_digest"] != content_digest
|
|
90
|
+
or evidence["media_type"] != media_type
|
|
91
|
+
or evidence["size_bytes"] != len(data)
|
|
92
|
+
or not isinstance(evidence["created_at"], str)
|
|
93
|
+
):
|
|
94
|
+
raise ArtifactError("artifact ingress returned mismatched evidence")
|
|
95
|
+
return ArtifactCommitment(
|
|
96
|
+
artifact_ref=artifact_ref, content_digest=content_digest, media_type=media_type
|
|
97
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""The front-door verbs, mirroring the npm ``ausca`` bin.
|
|
2
|
+
|
|
3
|
+
Configuration is environment only: a signing key and a mandatory per-call
|
|
4
|
+
USD cap for paying verbs, nothing else. Artifact commits are keyless. Output
|
|
5
|
+
is JSON on stdout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping, TextIO
|
|
16
|
+
|
|
17
|
+
from ausca.client import AuscaClient, ORIGIN
|
|
18
|
+
|
|
19
|
+
USAGE = """ausca: metered agent infrastructure services, paid per call
|
|
20
|
+
|
|
21
|
+
ausca catalog active offers, prices, routes
|
|
22
|
+
ausca price <offer-id> published price policy
|
|
23
|
+
ausca invoke <offer-id> [input] [--idempotency-key <key>]
|
|
24
|
+
paid invocation; input inline, @file, or stdin
|
|
25
|
+
ausca commit <file> artifact commitment for document and media offers
|
|
26
|
+
|
|
27
|
+
Environment: AUSCA_PRIVATE_KEY and AUSCA_MAX_PAYMENT_USD pay invocations;
|
|
28
|
+
AUSCA_NETWORK overrides the payment network; AUSCA_ORIGIN overrides the service.
|
|
29
|
+
The MCP server ships in the npm package: npx ausca mcp."""
|
|
30
|
+
|
|
31
|
+
MEDIA_TYPES = {
|
|
32
|
+
"pdf": "application/pdf",
|
|
33
|
+
"png": "image/png",
|
|
34
|
+
"jpg": "image/jpeg",
|
|
35
|
+
"jpeg": "image/jpeg",
|
|
36
|
+
"gif": "image/gif",
|
|
37
|
+
"webp": "image/webp",
|
|
38
|
+
"tif": "image/tiff",
|
|
39
|
+
"tiff": "image/tiff",
|
|
40
|
+
"txt": "text/plain",
|
|
41
|
+
"md": "text/markdown",
|
|
42
|
+
"html": "text/html",
|
|
43
|
+
"csv": "text/csv",
|
|
44
|
+
"json": "application/json",
|
|
45
|
+
"mp3": "audio/mpeg",
|
|
46
|
+
"wav": "audio/wav",
|
|
47
|
+
"m4a": "audio/mp4",
|
|
48
|
+
"flac": "audio/flac",
|
|
49
|
+
"ogg": "audio/ogg",
|
|
50
|
+
"mp4": "video/mp4",
|
|
51
|
+
"mov": "video/quicktime",
|
|
52
|
+
"webm": "video/webm",
|
|
53
|
+
"mkv": "video/x-matroska",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def media_type_for(name: str) -> str:
|
|
58
|
+
return MEDIA_TYPES.get(name.lower().rsplit(".", 1)[-1], "application/octet-stream")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _client(env: Mapping[str, str], *, require_payment: bool = False) -> AuscaClient:
|
|
62
|
+
origin = env.get("AUSCA_ORIGIN", ORIGIN)
|
|
63
|
+
key = env.get("AUSCA_PRIVATE_KEY")
|
|
64
|
+
if not key:
|
|
65
|
+
if require_payment:
|
|
66
|
+
raise SystemExit(
|
|
67
|
+
"paid invocations need AUSCA_PRIVATE_KEY and AUSCA_MAX_PAYMENT_USD"
|
|
68
|
+
" in the environment"
|
|
69
|
+
)
|
|
70
|
+
return AuscaClient(origin=origin)
|
|
71
|
+
try:
|
|
72
|
+
cap = float(env.get("AUSCA_MAX_PAYMENT_USD", ""))
|
|
73
|
+
except ValueError:
|
|
74
|
+
cap = 0.0
|
|
75
|
+
if cap <= 0:
|
|
76
|
+
raise SystemExit(
|
|
77
|
+
"AUSCA_MAX_PAYMENT_USD must be a positive USD amount when"
|
|
78
|
+
" AUSCA_PRIVATE_KEY is set; refusing to guess a spend limit"
|
|
79
|
+
)
|
|
80
|
+
return AuscaClient.with_local_key(
|
|
81
|
+
private_key=key,
|
|
82
|
+
max_payment_usd=cap,
|
|
83
|
+
network=env.get("AUSCA_NETWORK", "eip155:8453"),
|
|
84
|
+
origin=origin,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _json(value: Any) -> str:
|
|
89
|
+
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
90
|
+
value = dataclasses.asdict(value)
|
|
91
|
+
return json.dumps(value, indent=2, default=lambda item: dataclasses.asdict(item))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _resolve_input(argument: str | None, stdin: TextIO) -> dict[str, Any]:
|
|
95
|
+
if argument is None or argument == "-":
|
|
96
|
+
text = stdin.read()
|
|
97
|
+
elif argument.startswith("@"):
|
|
98
|
+
text = Path(argument[1:]).read_text()
|
|
99
|
+
else:
|
|
100
|
+
text = argument
|
|
101
|
+
try:
|
|
102
|
+
return json.loads(text)
|
|
103
|
+
except ValueError as error:
|
|
104
|
+
raise SystemExit("invocation input must be valid JSON") from error
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _invocation_args(argv: list[str]) -> tuple[str, str | None, str | None]:
|
|
108
|
+
if not argv:
|
|
109
|
+
raise SystemExit(
|
|
110
|
+
"usage: ausca invoke <offer-id> [input] [--idempotency-key <key>]"
|
|
111
|
+
)
|
|
112
|
+
offer_id = argv[0]
|
|
113
|
+
input_argument: str | None = None
|
|
114
|
+
idempotency_key: str | None = None
|
|
115
|
+
index = 1
|
|
116
|
+
while index < len(argv):
|
|
117
|
+
argument = argv[index]
|
|
118
|
+
if argument == "--idempotency-key":
|
|
119
|
+
if idempotency_key is not None or index + 1 >= len(argv):
|
|
120
|
+
raise SystemExit("--idempotency-key requires one value")
|
|
121
|
+
idempotency_key = argv[index + 1]
|
|
122
|
+
index += 2
|
|
123
|
+
elif input_argument is None:
|
|
124
|
+
input_argument = argument
|
|
125
|
+
index += 1
|
|
126
|
+
else:
|
|
127
|
+
raise SystemExit(f"unexpected invoke argument {argument}")
|
|
128
|
+
return offer_id, input_argument, idempotency_key
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def run(
|
|
132
|
+
argv: list[str],
|
|
133
|
+
env: Mapping[str, str],
|
|
134
|
+
stdout: TextIO,
|
|
135
|
+
stdin: TextIO,
|
|
136
|
+
) -> int:
|
|
137
|
+
verb = argv[0] if argv else None
|
|
138
|
+
if verb in (None, "help", "--help"):
|
|
139
|
+
print(USAGE, file=stdout)
|
|
140
|
+
return 1 if verb is None else 0
|
|
141
|
+
if verb == "catalog":
|
|
142
|
+
offers = _client(env).catalog().get("offers", [])
|
|
143
|
+
listing = [
|
|
144
|
+
{key: offer.get(key) for key in ("offer_id", "title", "route", "price", "revision")}
|
|
145
|
+
for offer in offers
|
|
146
|
+
]
|
|
147
|
+
print(_json(listing), file=stdout)
|
|
148
|
+
return 0
|
|
149
|
+
if verb == "price":
|
|
150
|
+
if len(argv) < 2:
|
|
151
|
+
raise SystemExit("usage: ausca price <offer-id>")
|
|
152
|
+
print(_json(_client(env).price(argv[1])), file=stdout)
|
|
153
|
+
return 0
|
|
154
|
+
if verb == "invoke":
|
|
155
|
+
offer_id, input_argument, idempotency_key = _invocation_args(argv[1:])
|
|
156
|
+
client = _client(env, require_payment=True)
|
|
157
|
+
outcome = client.invoke(
|
|
158
|
+
offer_id,
|
|
159
|
+
_resolve_input(input_argument, stdin),
|
|
160
|
+
idempotency_key=idempotency_key,
|
|
161
|
+
)
|
|
162
|
+
print(_json(outcome), file=stdout)
|
|
163
|
+
return 0
|
|
164
|
+
if verb == "commit":
|
|
165
|
+
if len(argv) < 2:
|
|
166
|
+
raise SystemExit("usage: ausca commit <file>")
|
|
167
|
+
path = Path(argv[1])
|
|
168
|
+
commitment = _client(env).commit(path.read_bytes(), media_type_for(path.name))
|
|
169
|
+
print(_json(commitment), file=stdout)
|
|
170
|
+
return 0
|
|
171
|
+
raise SystemExit(f"unknown verb {verb}; run ausca help")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def main() -> None:
|
|
175
|
+
try:
|
|
176
|
+
sys.exit(run(sys.argv[1:], os.environ, sys.stdout, sys.stdin))
|
|
177
|
+
except SystemExit:
|
|
178
|
+
raise
|
|
179
|
+
except Exception as error: # noqa: BLE001 - the CLI boundary reports and exits
|
|
180
|
+
print(str(error), file=sys.stderr)
|
|
181
|
+
sys.exit(1)
|