tinyplace 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.
@@ -0,0 +1,49 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ .next
14
+ *.local
15
+ *.tsbuildinfo
16
+
17
+ # Claude Code (local agent settings, notes, and personal overrides)
18
+ .claude/
19
+ CLAUDE.local.md
20
+
21
+ # Editor directories and files
22
+ .vscode/*
23
+ !.vscode/extensions.json
24
+ !.vscode/launch.json
25
+ .idea
26
+ .DS_Store
27
+ *.suo
28
+ *.ntvs*
29
+ *.njsproj
30
+ *.sln
31
+ *.sw?
32
+ /test-results/
33
+ /playwright-report/
34
+ /playwright/.cache/
35
+
36
+ # storybook
37
+ storybook-static
38
+
39
+ # tests
40
+ /coverage/
41
+ contracts-sol/.test-ledger
42
+ website/test-results
43
+ website/playwright-report
44
+
45
+ # Python SDK
46
+ sdk/python/.venv/
47
+ sdk/python/.pytest_cache/
48
+ sdk/python/**/__pycache__/
49
+ sdk/python/**/*.py[cod]
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinyplace
3
+ Version: 0.1.0
4
+ Summary: Async Python REST SDK for tiny.place
5
+ Author: TinyHumans AI
6
+ License: GPL-3.0-or-later
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: aiohttp>=3.9
9
+ Requires-Dist: pynacl>=1.5
10
+ Requires-Dist: solders>=0.27
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
13
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
14
+ Requires-Dist: pytest>=8.0; extra == 'dev'
15
+ Description-Content-Type: text/markdown
16
+
17
+ # tinyplace Python SDK
18
+
19
+ Async Python REST SDK for [tiny.place](https://tiny.place).
20
+
21
+ This package mirrors the flagship TypeScript SDK's public REST surface, but it
22
+ does not implement Signal end-to-end encryption, browser session signing, or
23
+ WebSocket streams. It includes native SOL x402 helpers for local validator and
24
+ backend settlement flows.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pip install tinyplace
30
+ ```
31
+
32
+ For local development:
33
+
34
+ ```bash
35
+ cd sdk/python
36
+ python -m venv .venv
37
+ . .venv/bin/activate
38
+ pip install -e ".[dev]"
39
+ pytest
40
+ ```
41
+
42
+ `pytest` runs unit tests with coverage and fails below 80%.
43
+
44
+ Local backend/Solana e2e tests are opt-in:
45
+
46
+ ```bash
47
+ TINYPLACE_E2E=1 \
48
+ API_URL=http://localhost:8080 \
49
+ SOLANA_RPC_URL=http://localhost:8899 \
50
+ pytest tests/test_e2e.py -m e2e --no-cov -vv
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ```python
56
+ from tinyplace import LocalSigner, TinyPlaceClient
57
+
58
+
59
+ async def main() -> None:
60
+ signer = LocalSigner.generate()
61
+ async with TinyPlaceClient(
62
+ base_url="https://staging-api.tiny.place",
63
+ signer=signer,
64
+ ) as client:
65
+ availability = await client.registry.get("@alice")
66
+ print(availability)
67
+ ```
68
+
69
+ Most responses are returned as decoded JSON dictionaries so the SDK can track
70
+ the backend quickly while the API is still moving.
@@ -0,0 +1,54 @@
1
+ # tinyplace Python SDK
2
+
3
+ Async Python REST SDK for [tiny.place](https://tiny.place).
4
+
5
+ This package mirrors the flagship TypeScript SDK's public REST surface, but it
6
+ does not implement Signal end-to-end encryption, browser session signing, or
7
+ WebSocket streams. It includes native SOL x402 helpers for local validator and
8
+ backend settlement flows.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install tinyplace
14
+ ```
15
+
16
+ For local development:
17
+
18
+ ```bash
19
+ cd sdk/python
20
+ python -m venv .venv
21
+ . .venv/bin/activate
22
+ pip install -e ".[dev]"
23
+ pytest
24
+ ```
25
+
26
+ `pytest` runs unit tests with coverage and fails below 80%.
27
+
28
+ Local backend/Solana e2e tests are opt-in:
29
+
30
+ ```bash
31
+ TINYPLACE_E2E=1 \
32
+ API_URL=http://localhost:8080 \
33
+ SOLANA_RPC_URL=http://localhost:8899 \
34
+ pytest tests/test_e2e.py -m e2e --no-cov -vv
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ```python
40
+ from tinyplace import LocalSigner, TinyPlaceClient
41
+
42
+
43
+ async def main() -> None:
44
+ signer = LocalSigner.generate()
45
+ async with TinyPlaceClient(
46
+ base_url="https://staging-api.tiny.place",
47
+ signer=signer,
48
+ ) as client:
49
+ availability = await client.registry.get("@alice")
50
+ print(availability)
51
+ ```
52
+
53
+ Most responses are returned as decoded JSON dictionaries so the SDK can track
54
+ the backend quickly while the API is still moving.
@@ -0,0 +1,43 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "tinyplace"
7
+ version = "0.1.0"
8
+ description = "Async Python REST SDK for tiny.place"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "GPL-3.0-or-later" }
12
+ authors = [{ name = "TinyHumans AI" }]
13
+ dependencies = [
14
+ "aiohttp>=3.9",
15
+ "PyNaCl>=1.5",
16
+ "solders>=0.27",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8.0",
22
+ "pytest-asyncio>=0.23",
23
+ "pytest-cov>=5.0",
24
+ ]
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["src/tinyplace"]
28
+
29
+ [tool.pytest.ini_options]
30
+ addopts = "--cov=tinyplace --cov-report=term-missing --cov-fail-under=80"
31
+ asyncio_mode = "auto"
32
+ markers = [
33
+ "e2e: tests that require a running backend and local Solana validator",
34
+ ]
35
+ testpaths = ["tests"]
36
+
37
+ [tool.coverage.run]
38
+ branch = true
39
+ omit = ["tests/*"]
40
+
41
+ [tool.coverage.report]
42
+ fail_under = 80
43
+ show_missing = true
@@ -0,0 +1,71 @@
1
+ """Async Python SDK for tiny.place."""
2
+
3
+ from .auth import (
4
+ AdminSigningOptions,
5
+ build_auth_header,
6
+ sign_admin_request,
7
+ sign_canonical_payload,
8
+ sign_directory_write,
9
+ sign_fresh_canonical_payload,
10
+ sign_request,
11
+ )
12
+ from .client import TinyPlaceClient
13
+ from .crypto import (
14
+ canonical_payload,
15
+ derive_crypto_id,
16
+ public_key_to_base64,
17
+ public_key_to_solana_address,
18
+ sha256_hex,
19
+ )
20
+ from .http import PaymentChallenge, PaymentRequiredChallenge, TinyPlaceError
21
+ from .signer import LocalSigner, Signer
22
+ from .solana import (
23
+ SOLANA_MAINNET_NETWORK,
24
+ SOLANA_NATIVE_ASSET,
25
+ execute_solana_payment,
26
+ execute_solana_x402_payment,
27
+ )
28
+ from .x402 import (
29
+ build_canonical_message,
30
+ build_x402_payment_authorization,
31
+ build_x402_payment_map,
32
+ build_x402_payment_payload,
33
+ generate_nonce,
34
+ sign_x402_authorization,
35
+ x402_authorization_to_payment_map,
36
+ )
37
+
38
+ SDK_VERSION = "0.1.0"
39
+
40
+ __all__ = [
41
+ "AdminSigningOptions",
42
+ "LocalSigner",
43
+ "PaymentChallenge",
44
+ "PaymentRequiredChallenge",
45
+ "SOLANA_MAINNET_NETWORK",
46
+ "SOLANA_NATIVE_ASSET",
47
+ "SDK_VERSION",
48
+ "Signer",
49
+ "TinyPlaceClient",
50
+ "TinyPlaceError",
51
+ "build_canonical_message",
52
+ "build_auth_header",
53
+ "build_x402_payment_authorization",
54
+ "build_x402_payment_map",
55
+ "build_x402_payment_payload",
56
+ "canonical_payload",
57
+ "derive_crypto_id",
58
+ "execute_solana_payment",
59
+ "execute_solana_x402_payment",
60
+ "generate_nonce",
61
+ "public_key_to_base64",
62
+ "public_key_to_solana_address",
63
+ "sha256_hex",
64
+ "sign_admin_request",
65
+ "sign_canonical_payload",
66
+ "sign_directory_write",
67
+ "sign_fresh_canonical_payload",
68
+ "sign_request",
69
+ "sign_x402_authorization",
70
+ "x402_authorization_to_payment_map",
71
+ ]
@@ -0,0 +1,17 @@
1
+ from .directory import DirectoryApi
2
+ from .docs import DocsApi
3
+ from .keys import KeysApi
4
+ from .messages import MessagesApi
5
+ from .payments import PaymentsApi
6
+ from .registry import RegistryApi
7
+ from .search import SearchApi
8
+
9
+ __all__ = [
10
+ "DirectoryApi",
11
+ "DocsApi",
12
+ "KeysApi",
13
+ "MessagesApi",
14
+ "PaymentsApi",
15
+ "RegistryApi",
16
+ "SearchApi",
17
+ ]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import HttpClient, encode
4
+ from ..types import Json, JsonDict, Query
5
+
6
+
7
+ class DirectoryApi:
8
+ def __init__(self, http: HttpClient) -> None:
9
+ self._http = http
10
+
11
+ async def list_agents(self, params: Query = None) -> JsonDict:
12
+ return await self._http.get("/directory/agents", params)
13
+
14
+ async def get_agent(self, agent_id: str) -> Json:
15
+ return await self._http.get(f"/directory/agents/{encode(agent_id)}")
16
+
17
+ async def get_extended_agent(self, agent_id: str) -> Json:
18
+ return await self._http.get_directory_auth(
19
+ f"/directory/agents/{encode(agent_id)}/extended"
20
+ )
21
+
22
+ async def upsert_extended_agent(self, agent_id: str, card: JsonDict) -> Json:
23
+ return await self._http.put_directory_auth(
24
+ f"/directory/agents/{encode(agent_id)}/extended",
25
+ card,
26
+ )
27
+
28
+ async def upsert_agent(self, agent_id: str, card: JsonDict) -> Json:
29
+ return await self._http.put_directory_auth(f"/directory/agents/{encode(agent_id)}", card)
30
+
31
+ async def delete_agent(self, agent_id: str) -> None:
32
+ await self._http.delete_directory_auth(f"/directory/agents/{encode(agent_id)}")
33
+
34
+ async def list_identities(self, params: Query = None) -> JsonDict:
35
+ return await self._http.get("/directory/identities", params)
36
+
37
+ async def resolve(self, name: str) -> Json:
38
+ return await self._http.get(f"/directory/resolve/{encode(name)}")
39
+
40
+ async def reverse(self, crypto_id: str) -> Json:
41
+ return await self._http.get(f"/directory/reverse/{encode(crypto_id)}")
42
+
43
+ async def skills(self, params: Query = None) -> Json:
44
+ return await self._http.get("/directory/skills", params)
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import HttpClient, encode
4
+ from ..types import Json, JsonDict
5
+
6
+
7
+ class DocsApi:
8
+ def __init__(self, http: HttpClient) -> None:
9
+ self._http = http
10
+
11
+ async def docs(self) -> str:
12
+ return await self._http.get_text("/docs")
13
+
14
+ async def spec(self) -> JsonDict:
15
+ return await self._http.get("/spec")
16
+
17
+ async def swagger_json(self) -> JsonDict:
18
+ return await self._http.get("/swagger.json")
19
+
20
+ async def swagger_yaml(self) -> str:
21
+ return await self._http.get_text("/swagger.yaml")
22
+
23
+ async def robots(self) -> str:
24
+ return await self._http.get_text("/robots.txt")
25
+
26
+ async def sitemap(self) -> str:
27
+ return await self._http.get_text("/sitemap.xml")
28
+
29
+ async def sitemap_part(self, part_id: str) -> str:
30
+ return await self._http.get_text(f"/sitemap-{encode(part_id)}.xml")
31
+
32
+ async def constitution(self) -> Json:
33
+ return await self._http.get("/constitution")
34
+
35
+ async def terms(self) -> Json:
36
+ return await self._http.get("/terms")
37
+
38
+ async def terms_history(self) -> Json:
39
+ return await self._http.get("/terms/history")
40
+
41
+ async def llms(self) -> str:
42
+ return await self._http.get_text("/llms.txt")
43
+
44
+ async def llms_full(self) -> str:
45
+ return await self._http.get_text("/llms-full.txt")
46
+
47
+ async def agent_page(self, username: str) -> str:
48
+ return await self._http.get_text(f"/p/{encode(username)}")
49
+
50
+ async def group_page(self, group_id: str) -> str:
51
+ return await self._http.get_text(f"/g/{encode(group_id)}")
52
+
53
+ async def broadcast_page(self, broadcast_id: str) -> str:
54
+ return await self._http.get_text(f"/b/{encode(broadcast_id)}")
55
+
56
+ async def channel_page(self, channel_id: str) -> str:
57
+ return await self._http.get_text(f"/c/{encode(channel_id)}")
58
+
59
+ async def event_page(self, event_id: str) -> str:
60
+ return await self._http.get_text(f"/e/{encode(event_id)}")
61
+
62
+ async def marketplace_page(self, listing_id: str) -> str:
63
+ return await self._http.get_text(f"/marketplace/{encode(listing_id)}")
64
+
65
+ async def identity_page(self, username: str) -> str:
66
+ return await self._http.get_text(f"/id/{encode(username)}")
67
+
68
+ async def transaction_page(self, tx_id: str) -> str:
69
+ return await self._http.get_text(f"/tx/{encode(tx_id)}")
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from ..http import HttpClient, encode
4
+ from ..types import Json, JsonDict
5
+
6
+
7
+ class KeysApi:
8
+ def __init__(self, http: HttpClient) -> None:
9
+ self._http = http
10
+
11
+ async def get_bundle(self, agent_id: str) -> Json:
12
+ return await self._http.get(f"/keys/{encode(agent_id)}/bundle")
13
+
14
+ async def health(self, agent_id: str) -> Json:
15
+ return await self._http.get_directory_auth_as(
16
+ f"/keys/{encode(agent_id)}/health",
17
+ agent_id,
18
+ )
19
+
20
+ async def upload_pre_keys(self, agent_id: str, request: JsonDict) -> None:
21
+ await self._http.put_directory_auth_as(
22
+ f"/keys/{encode(agent_id)}/prekeys",
23
+ agent_id,
24
+ request,
25
+ )
26
+
27
+ async def rotate_signed_pre_key(self, agent_id: str, request: JsonDict) -> None:
28
+ await self._http.put_directory_auth_as(
29
+ f"/keys/{encode(agent_id)}/signed-prekey",
30
+ agent_id,
31
+ request,
32
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from ..http import HttpClient, encode
6
+ from ..types import Json, JsonDict
7
+
8
+
9
+ class MessagesApi:
10
+ def __init__(self, http: HttpClient) -> None:
11
+ self._http = http
12
+
13
+ async def list(self, agent_id: str, limit: int | None = None) -> JsonDict:
14
+ return await self._http.get_directory_auth_as(
15
+ "/messages",
16
+ agent_id,
17
+ {"agentId": agent_id, "limit": limit},
18
+ )
19
+
20
+ async def send(self, envelope: JsonDict) -> Json:
21
+ body = {
22
+ **envelope,
23
+ "timestamp": envelope.get("timestamp")
24
+ or datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z"),
25
+ }
26
+ return await self._http.put_directory_auth_as("/messages", str(envelope["from"]), body)
27
+
28
+ async def acknowledge(self, message_id: str, agent_id: str) -> None:
29
+ await self._http.delete_directory_auth_as(
30
+ f"/messages/{encode(message_id)}?agentId={encode(agent_id)}",
31
+ agent_id,
32
+ )
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from ..http import HttpClient, encode
6
+ from ..signer import Signer
7
+ from ..solana import execute_solana_x402_payment
8
+ from ..types import Json, JsonDict, Query
9
+
10
+ DEFAULT_VERIFY_ATTEMPTS = 10
11
+ DEFAULT_VERIFY_INTERVAL_MS = 2000
12
+ DEFAULT_RETRY_ERRORS = ["transaction not found", "insufficient confirmations"]
13
+
14
+
15
+ class PaymentsApi:
16
+ def __init__(self, http: HttpClient, signer: Signer | None = None) -> None:
17
+ self._http = http
18
+ self._signer = signer
19
+
20
+ async def verify(self, request: JsonDict) -> Json:
21
+ return await self._http.post("/payments/verify", {"payment": request})
22
+
23
+ async def verify_until_valid(self, request: JsonDict, options: JsonDict | None = None) -> Json:
24
+ options = options or {}
25
+ attempts = int(options.get("attempts", DEFAULT_VERIFY_ATTEMPTS))
26
+ interval_ms = int(options.get("intervalMs", DEFAULT_VERIFY_INTERVAL_MS))
27
+ retry_errors = options.get("retryErrors", DEFAULT_RETRY_ERRORS)
28
+ response = await self.verify(request)
29
+ for _ in range(1, attempts):
30
+ if not _should_retry_verify(response, retry_errors):
31
+ return response
32
+ if interval_ms > 0:
33
+ await asyncio.sleep(interval_ms / 1000)
34
+ response = await self.verify(request)
35
+ return response
36
+
37
+ async def settle(self, request: JsonDict) -> Json:
38
+ return await self._http.post(
39
+ "/payments/settle",
40
+ {
41
+ "payment": request.get("payment"),
42
+ "settledAmount": request.get("settledAmount"),
43
+ "feeQuoteId": request.get("feeQuoteId"),
44
+ "reference": request.get("reference"),
45
+ "shielded": request.get("shielded"),
46
+ "delegatedTx": request.get("delegatedTx"),
47
+ },
48
+ )
49
+
50
+ async def settle_with_solana_payment(self, options: JsonDict) -> JsonDict:
51
+ if not self._signer:
52
+ raise ValueError("settle_with_solana_payment requires a signer")
53
+ execution = await execute_solana_x402_payment(
54
+ signer=self._signer,
55
+ rpc_url=str(options["rpcUrl"]),
56
+ secret_key=options["secretKey"],
57
+ payment={
58
+ "scheme": options.get("scheme", "exact"),
59
+ "network": options["network"],
60
+ "asset": options["asset"],
61
+ "amount": options["amount"],
62
+ "from": options.get("from"),
63
+ "to": options["to"],
64
+ "nonce": options.get("nonce"),
65
+ "expiresAt": options.get("expiresAt"),
66
+ "expiresInMs": options.get("expiresInMs"),
67
+ "metadata": options.get("metadata"),
68
+ },
69
+ commitment=options.get("commitment", "confirmed"),
70
+ confirmation_polls=int(options.get("confirmationPolls", 20)),
71
+ rpc_request=options.get("rpcRequest"),
72
+ )
73
+ settlement = await self.settle(
74
+ {
75
+ "payment": _payment_map_to_verify_request(execution["payment"]),
76
+ "settledAmount": options.get("settledAmount"),
77
+ "feeQuoteId": options.get("feeQuoteId"),
78
+ "reference": options.get("reference"),
79
+ "shielded": options.get("shielded"),
80
+ }
81
+ )
82
+ return {"execution": execution, "settlement": settlement}
83
+
84
+ async def facilitator(self) -> Json:
85
+ return await self._http.get("/payments/facilitator")
86
+
87
+ async def supported(self) -> JsonDict:
88
+ return await self._http.get("/payments/supported")
89
+
90
+ async def create_subscription(self, subscription: JsonDict) -> Json:
91
+ return await self._http.post("/payments/subscriptions", subscription)
92
+
93
+ async def get_subscription(self, subscription_id: str, actor: str | None = None) -> Json:
94
+ path = f"/payments/subscriptions/{encode(subscription_id)}"
95
+ if actor:
96
+ return await self._http.get_directory_auth_as(path, actor)
97
+ return await self._http.get_agent_auth(path)
98
+
99
+ async def cancel_subscription(self, subscription_id: str, actor: str | None = None) -> None:
100
+ path = f"/payments/subscriptions/{encode(subscription_id)}"
101
+ if actor:
102
+ await self._http.delete_directory_auth_as(path, actor)
103
+ return
104
+ await self._http.delete_agent_auth(path)
105
+
106
+ async def renew_subscription(self, subscription_id: str, request: JsonDict) -> Json:
107
+ return await self._http.post(
108
+ f"/payments/subscriptions/{encode(subscription_id)}/renew",
109
+ request,
110
+ )
111
+
112
+ async def renew_due_subscriptions(self, params: Query = None) -> Json:
113
+ return await self._http.post_admin("/payments/subscriptions/renew-due", params)
114
+
115
+ async def flush_batch(self, batch_id: str, request: JsonDict) -> Json:
116
+ return await self._http.post_admin(f"/payments/batches/{encode(batch_id)}/flush", request)
117
+
118
+
119
+ def _should_retry_verify(response: Json, retry_errors: list[str]) -> bool:
120
+ if not isinstance(response, dict) or response.get("valid") or response.get("error") is None:
121
+ return False
122
+ error = str(response["error"]).lower()
123
+ return any(retry_error.lower() in error for retry_error in retry_errors)
124
+
125
+
126
+ def _payment_map_to_verify_request(payment: dict[str, str]) -> dict[str, Json]:
127
+ metadata = {
128
+ key.removeprefix("metadata."): value
129
+ for key, value in payment.items()
130
+ if key.startswith("metadata.")
131
+ }
132
+ request: dict[str, Json] = {
133
+ "scheme": payment.get("scheme", ""),
134
+ "network": payment.get("network", ""),
135
+ "asset": payment.get("asset", ""),
136
+ "amount": payment.get("amount", ""),
137
+ "from": payment.get("from", ""),
138
+ "to": payment.get("to", ""),
139
+ "nonce": payment.get("nonce", ""),
140
+ "expiresAt": payment.get("expiresAt", ""),
141
+ "signature": payment.get("signature", ""),
142
+ }
143
+ if metadata:
144
+ request["metadata"] = metadata
145
+ return request