rare-platform-sdk 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.
- rare_platform_sdk-0.1.0/PKG-INFO +90 -0
- rare_platform_sdk-0.1.0/README.md +69 -0
- rare_platform_sdk-0.1.0/pyproject.toml +39 -0
- rare_platform_sdk-0.1.0/setup.cfg +4 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/__init__.py +58 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/client.py +108 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/fastapi.py +84 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/kit.py +308 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/stores.py +130 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk/types.py +154 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk.egg-info/PKG-INFO +90 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk.egg-info/SOURCES.txt +15 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk.egg-info/dependency_links.txt +1 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk.egg-info/requires.txt +10 -0
- rare_platform_sdk-0.1.0/src/rare_platform_sdk.egg-info/top_level.txt +1 -0
- rare_platform_sdk-0.1.0/tests/test_client.py +72 -0
- rare_platform_sdk-0.1.0/tests/test_kit.py +486 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rare-platform-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rare platform integration kit for Python services
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://rareid.cc
|
|
7
|
+
Project-URL: Repository, https://github.com/Rare-ID/Rare
|
|
8
|
+
Project-URL: Documentation, https://github.com/Rare-ID/Rare/tree/main/packages/platform/python/rare-platform-sdk-python
|
|
9
|
+
Project-URL: Issues, https://github.com/Rare-ID/Rare/issues
|
|
10
|
+
Keywords: rare,platform,identity,delegation,attestation
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: fastapi>=0.115.0
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Requires-Dist: rare-identity-protocol>=0.1.0
|
|
16
|
+
Requires-Dist: rare-identity-verifier>=0.1.0
|
|
17
|
+
Provides-Extra: redis
|
|
18
|
+
Requires-Dist: redis>=5.0.0; extra == "redis"
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest>=8.2.0; extra == "test"
|
|
21
|
+
|
|
22
|
+
# rare-platform-sdk
|
|
23
|
+
|
|
24
|
+
Python toolkit for third-party platforms integrating Rare with local verification-first defaults.
|
|
25
|
+
|
|
26
|
+
## What It Is
|
|
27
|
+
|
|
28
|
+
`rare-platform-sdk` helps Python services issue Rare auth challenges, complete login, verify delegated signed actions, manage platform sessions, and optionally ingest signed negative-event signals back into Rare.
|
|
29
|
+
|
|
30
|
+
## Who It Is For
|
|
31
|
+
|
|
32
|
+
- Python and FastAPI platforms adding Rare login
|
|
33
|
+
- Backend teams that want local identity/delegation verification
|
|
34
|
+
- Integrators that need Redis-backed replay protection and session storage
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install rare-platform-sdk
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from rare_platform_sdk import (
|
|
44
|
+
InMemoryChallengeStore,
|
|
45
|
+
InMemoryReplayStore,
|
|
46
|
+
InMemorySessionStore,
|
|
47
|
+
RareApiClient,
|
|
48
|
+
RarePlatformKitConfig,
|
|
49
|
+
create_rare_platform_kit,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
rare = RareApiClient(rare_base_url="https://api.rareid.cc")
|
|
53
|
+
kit = create_rare_platform_kit(
|
|
54
|
+
RarePlatformKitConfig(
|
|
55
|
+
aud="platform",
|
|
56
|
+
rare_api_client=rare,
|
|
57
|
+
challenge_store=InMemoryChallengeStore(),
|
|
58
|
+
replay_store=InMemoryReplayStore(),
|
|
59
|
+
session_store=InMemorySessionStore(),
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
FastAPI integration:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from fastapi import FastAPI
|
|
68
|
+
from rare_platform_sdk import create_fastapi_rare_router
|
|
69
|
+
|
|
70
|
+
app = FastAPI()
|
|
71
|
+
app.include_router(create_fastapi_rare_router(kit, prefix="/rare"))
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Production Notes
|
|
75
|
+
|
|
76
|
+
- Challenge nonces must be one-time use.
|
|
77
|
+
- Delegation replay protection must be atomic.
|
|
78
|
+
- Full identity mode requires `payload.aud == expected_aud`.
|
|
79
|
+
- Identity triad must match:
|
|
80
|
+
`auth_complete.agent_id == delegation.agent_id == attestation.sub`
|
|
81
|
+
- Public identity mode is capped to `L1` effective governance.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install -r requirements-test.lock
|
|
87
|
+
pip install -e .[test] --no-deps
|
|
88
|
+
pytest -q
|
|
89
|
+
python -m build
|
|
90
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# rare-platform-sdk
|
|
2
|
+
|
|
3
|
+
Python toolkit for third-party platforms integrating Rare with local verification-first defaults.
|
|
4
|
+
|
|
5
|
+
## What It Is
|
|
6
|
+
|
|
7
|
+
`rare-platform-sdk` helps Python services issue Rare auth challenges, complete login, verify delegated signed actions, manage platform sessions, and optionally ingest signed negative-event signals back into Rare.
|
|
8
|
+
|
|
9
|
+
## Who It Is For
|
|
10
|
+
|
|
11
|
+
- Python and FastAPI platforms adding Rare login
|
|
12
|
+
- Backend teams that want local identity/delegation verification
|
|
13
|
+
- Integrators that need Redis-backed replay protection and session storage
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install rare-platform-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from rare_platform_sdk import (
|
|
23
|
+
InMemoryChallengeStore,
|
|
24
|
+
InMemoryReplayStore,
|
|
25
|
+
InMemorySessionStore,
|
|
26
|
+
RareApiClient,
|
|
27
|
+
RarePlatformKitConfig,
|
|
28
|
+
create_rare_platform_kit,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
rare = RareApiClient(rare_base_url="https://api.rareid.cc")
|
|
32
|
+
kit = create_rare_platform_kit(
|
|
33
|
+
RarePlatformKitConfig(
|
|
34
|
+
aud="platform",
|
|
35
|
+
rare_api_client=rare,
|
|
36
|
+
challenge_store=InMemoryChallengeStore(),
|
|
37
|
+
replay_store=InMemoryReplayStore(),
|
|
38
|
+
session_store=InMemorySessionStore(),
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
FastAPI integration:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from fastapi import FastAPI
|
|
47
|
+
from rare_platform_sdk import create_fastapi_rare_router
|
|
48
|
+
|
|
49
|
+
app = FastAPI()
|
|
50
|
+
app.include_router(create_fastapi_rare_router(kit, prefix="/rare"))
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Production Notes
|
|
54
|
+
|
|
55
|
+
- Challenge nonces must be one-time use.
|
|
56
|
+
- Delegation replay protection must be atomic.
|
|
57
|
+
- Full identity mode requires `payload.aud == expected_aud`.
|
|
58
|
+
- Identity triad must match:
|
|
59
|
+
`auth_complete.agent_id == delegation.agent_id == attestation.sub`
|
|
60
|
+
- Public identity mode is capped to `L1` effective governance.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install -r requirements-test.lock
|
|
66
|
+
pip install -e .[test] --no-deps
|
|
67
|
+
pytest -q
|
|
68
|
+
python -m build
|
|
69
|
+
```
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rare-platform-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Rare platform integration kit for Python services"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"fastapi>=0.115.0",
|
|
14
|
+
"httpx>=0.27.0",
|
|
15
|
+
"rare-identity-protocol>=0.1.0",
|
|
16
|
+
"rare-identity-verifier>=0.1.0",
|
|
17
|
+
]
|
|
18
|
+
keywords = ["rare", "platform", "identity", "delegation", "attestation"]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
redis = [
|
|
22
|
+
"redis>=5.0.0",
|
|
23
|
+
]
|
|
24
|
+
test = [
|
|
25
|
+
"pytest>=8.2.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://rareid.cc"
|
|
30
|
+
Repository = "https://github.com/Rare-ID/Rare"
|
|
31
|
+
Documentation = "https://github.com/Rare-ID/Rare/tree/main/packages/platform/python/rare-platform-sdk-python"
|
|
32
|
+
Issues = "https://github.com/Rare-ID/Rare/issues"
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
include = ["rare_platform_sdk*"]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from rare_platform_sdk.client import ApiError, RareApiClient, RareApiClientError
|
|
2
|
+
from rare_platform_sdk.fastapi import (
|
|
3
|
+
AuthChallengeRequest,
|
|
4
|
+
AuthChallengeResponse,
|
|
5
|
+
AuthCompleteRequest,
|
|
6
|
+
AuthCompleteResponse,
|
|
7
|
+
create_fastapi_rare_router,
|
|
8
|
+
)
|
|
9
|
+
from rare_platform_sdk.kit import create_rare_platform_kit, sign_platform_event_token
|
|
10
|
+
from rare_platform_sdk.stores import (
|
|
11
|
+
InMemoryChallengeStore,
|
|
12
|
+
InMemoryReplayStore,
|
|
13
|
+
InMemorySessionStore,
|
|
14
|
+
RedisChallengeStore,
|
|
15
|
+
RedisReplayStore,
|
|
16
|
+
RedisSessionStore,
|
|
17
|
+
)
|
|
18
|
+
from rare_platform_sdk.types import (
|
|
19
|
+
AuthChallenge,
|
|
20
|
+
AuthCompleteInput,
|
|
21
|
+
AuthCompleteResult,
|
|
22
|
+
IngestEventsInput,
|
|
23
|
+
IngestEventsResult,
|
|
24
|
+
PlatformSession,
|
|
25
|
+
RarePlatformEventItem,
|
|
26
|
+
RarePlatformKitConfig,
|
|
27
|
+
VerifiedActionContext,
|
|
28
|
+
VerifyActionInput,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ApiError",
|
|
33
|
+
"AuthChallenge",
|
|
34
|
+
"AuthChallengeRequest",
|
|
35
|
+
"AuthChallengeResponse",
|
|
36
|
+
"AuthCompleteInput",
|
|
37
|
+
"AuthCompleteRequest",
|
|
38
|
+
"AuthCompleteResponse",
|
|
39
|
+
"AuthCompleteResult",
|
|
40
|
+
"InMemoryChallengeStore",
|
|
41
|
+
"InMemoryReplayStore",
|
|
42
|
+
"InMemorySessionStore",
|
|
43
|
+
"IngestEventsInput",
|
|
44
|
+
"IngestEventsResult",
|
|
45
|
+
"PlatformSession",
|
|
46
|
+
"RareApiClient",
|
|
47
|
+
"RareApiClientError",
|
|
48
|
+
"RarePlatformEventItem",
|
|
49
|
+
"RarePlatformKitConfig",
|
|
50
|
+
"RedisChallengeStore",
|
|
51
|
+
"RedisReplayStore",
|
|
52
|
+
"RedisSessionStore",
|
|
53
|
+
"VerifiedActionContext",
|
|
54
|
+
"VerifyActionInput",
|
|
55
|
+
"create_fastapi_rare_router",
|
|
56
|
+
"create_rare_platform_kit",
|
|
57
|
+
"sign_platform_event_token",
|
|
58
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RareApiClientError(RuntimeError):
|
|
10
|
+
"""Raised for Rare API failures."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class ApiError(RareApiClientError):
|
|
15
|
+
status_code: int
|
|
16
|
+
detail: str
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str:
|
|
19
|
+
return f"rare api error {self.status_code}: {self.detail}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RareApiClient:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
rare_base_url: str,
|
|
27
|
+
http_client: httpx.AsyncClient | None = None,
|
|
28
|
+
default_headers: dict[str, str] | None = None,
|
|
29
|
+
timeout_seconds: float = 10.0,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.rare_base_url = rare_base_url.rstrip("/")
|
|
32
|
+
self.default_headers = default_headers or {}
|
|
33
|
+
self._owns_http_client = http_client is None
|
|
34
|
+
self._http = http_client or httpx.AsyncClient(timeout=timeout_seconds)
|
|
35
|
+
|
|
36
|
+
async def aclose(self) -> None:
|
|
37
|
+
if self._owns_http_client:
|
|
38
|
+
await self._http.aclose()
|
|
39
|
+
|
|
40
|
+
async def get_jwks(self) -> dict[str, Any]:
|
|
41
|
+
return await self._request_json("GET", "/.well-known/rare-keys.json")
|
|
42
|
+
|
|
43
|
+
async def issue_platform_register_challenge(
|
|
44
|
+
self, *, platform_aud: str, domain: str
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
return await self._request_json(
|
|
47
|
+
"POST",
|
|
48
|
+
"/v1/platforms/register/challenge",
|
|
49
|
+
{"platform_aud": platform_aud, "domain": domain},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def complete_platform_register(
|
|
53
|
+
self,
|
|
54
|
+
*,
|
|
55
|
+
challenge_id: str,
|
|
56
|
+
platform_id: str,
|
|
57
|
+
platform_aud: str,
|
|
58
|
+
domain: str,
|
|
59
|
+
keys: list[dict[str, str]],
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
return await self._request_json(
|
|
62
|
+
"POST",
|
|
63
|
+
"/v1/platforms/register/complete",
|
|
64
|
+
{
|
|
65
|
+
"challenge_id": challenge_id,
|
|
66
|
+
"platform_id": platform_id,
|
|
67
|
+
"platform_aud": platform_aud,
|
|
68
|
+
"domain": domain,
|
|
69
|
+
"keys": keys,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def ingest_platform_events(self, event_token: str) -> dict[str, Any]:
|
|
74
|
+
return await self._request_json(
|
|
75
|
+
"POST",
|
|
76
|
+
"/v1/identity-library/events/ingest",
|
|
77
|
+
{"event_token": event_token},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
async def _request_json(
|
|
81
|
+
self, method: str, path: str, body: dict[str, Any] | None = None
|
|
82
|
+
) -> dict[str, Any]:
|
|
83
|
+
response = await self._http.request(
|
|
84
|
+
method,
|
|
85
|
+
f"{self.rare_base_url}{path}",
|
|
86
|
+
headers={
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
**self.default_headers,
|
|
89
|
+
},
|
|
90
|
+
json=body,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
payload: dict[str, Any] | str
|
|
94
|
+
try:
|
|
95
|
+
payload = response.json()
|
|
96
|
+
except Exception: # noqa: BLE001
|
|
97
|
+
payload = response.text
|
|
98
|
+
|
|
99
|
+
if response.status_code >= 400:
|
|
100
|
+
if isinstance(payload, dict):
|
|
101
|
+
detail = str(payload.get("detail") or payload)
|
|
102
|
+
else:
|
|
103
|
+
detail = payload
|
|
104
|
+
raise ApiError(status_code=response.status_code, detail=detail)
|
|
105
|
+
|
|
106
|
+
if not isinstance(payload, dict):
|
|
107
|
+
raise RareApiClientError("expected JSON object response")
|
|
108
|
+
return payload
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, HTTPException
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from rare_identity_protocol import SignatureError, TokenValidationError
|
|
10
|
+
|
|
11
|
+
from rare_platform_sdk.types import AuthCompleteInput, RarePlatformKit
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthChallengeRequest(BaseModel):
|
|
15
|
+
aud: str | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthChallengeResponse(BaseModel):
|
|
19
|
+
nonce: str
|
|
20
|
+
aud: str
|
|
21
|
+
issued_at: int
|
|
22
|
+
expires_at: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AuthCompleteRequest(BaseModel):
|
|
26
|
+
nonce: str
|
|
27
|
+
agent_id: str
|
|
28
|
+
session_pubkey: str
|
|
29
|
+
delegation_token: str
|
|
30
|
+
signature_by_session: str
|
|
31
|
+
public_identity_attestation: str | None = None
|
|
32
|
+
full_identity_attestation: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AuthCompleteResponse(BaseModel):
|
|
36
|
+
session_token: str
|
|
37
|
+
agent_id: str
|
|
38
|
+
level: str
|
|
39
|
+
raw_level: str
|
|
40
|
+
identity_mode: str
|
|
41
|
+
display_name: str
|
|
42
|
+
session_pubkey: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _raise_http(exc: Exception) -> None:
|
|
46
|
+
if isinstance(exc, PermissionError):
|
|
47
|
+
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
|
48
|
+
if isinstance(exc, (TokenValidationError, SignatureError, ValueError)):
|
|
49
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
50
|
+
raise HTTPException(status_code=500, detail="internal server error") from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_fastapi_rare_router(
|
|
54
|
+
kit: RarePlatformKit, prefix: str = ""
|
|
55
|
+
) -> APIRouter:
|
|
56
|
+
router = APIRouter(prefix=prefix)
|
|
57
|
+
|
|
58
|
+
@router.post("/auth/challenge", response_model=AuthChallengeResponse)
|
|
59
|
+
async def auth_challenge(request: AuthChallengeRequest) -> AuthChallengeResponse:
|
|
60
|
+
try:
|
|
61
|
+
challenge = await kit.issue_challenge(request.aud)
|
|
62
|
+
return AuthChallengeResponse(**asdict(challenge))
|
|
63
|
+
except Exception as exc: # noqa: BLE001
|
|
64
|
+
_raise_http(exc)
|
|
65
|
+
|
|
66
|
+
@router.post("/auth/complete", response_model=AuthCompleteResponse)
|
|
67
|
+
async def auth_complete(request: AuthCompleteRequest) -> AuthCompleteResponse:
|
|
68
|
+
try:
|
|
69
|
+
result = await kit.complete_auth(
|
|
70
|
+
AuthCompleteInput(
|
|
71
|
+
nonce=request.nonce,
|
|
72
|
+
agent_id=request.agent_id,
|
|
73
|
+
session_pubkey=request.session_pubkey,
|
|
74
|
+
delegation_token=request.delegation_token,
|
|
75
|
+
signature_by_session=request.signature_by_session,
|
|
76
|
+
public_identity_attestation=request.public_identity_attestation,
|
|
77
|
+
full_identity_attestation=request.full_identity_attestation,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return AuthCompleteResponse(**asdict(result))
|
|
81
|
+
except Exception as exc: # noqa: BLE001
|
|
82
|
+
_raise_http(exc)
|
|
83
|
+
|
|
84
|
+
return router
|