rightos-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.
@@ -0,0 +1,16 @@
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ # 永続化データ(トークンのハッシュ等を含むためコミットしない)
5
+ .data/
6
+ *.log
7
+ .DS_Store
8
+ .env*
9
+ !.env.example
10
+ next-env.d.ts
11
+ *.tsbuildinfo
12
+ # YouTubeアップロード用の秘密情報(コミット禁止)
13
+ video/oauth-client.json
14
+ video/token.json
15
+ video/.venv/
16
+ video/work/
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: rightos-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for RightOS - privacy-preserving rights verification API (digital QR tickets for queues, reservations, EV charging, package pickup).
5
+ Project-URL: Homepage, https://rightos.i-s3.com/software/rightos/docs
6
+ Project-URL: Repository, https://github.com/suomimasuda/rightos-sdk
7
+ Project-URL: API Spec, https://rightos.i-s3.com/openapi.json
8
+ Author: I-S3 Software Division
9
+ License: MIT
10
+ Keywords: digital-ticket,i-s3,qr,queue,rightos,verification
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Requires-Python: >=3.9
16
+ Description-Content-Type: text/markdown
17
+
18
+ # rightos-sdk — RightOS Python SDK
19
+
20
+ Official Python SDK for [RightOS](https://rightos.i-s3.com/software/rightos) — privacy-preserving rights verification infrastructure. Issue and verify digital QR tickets ("Right Tokens") for queues, reservations, EV charging, and package pickup, without ever collecting end users' names or phone numbers.
21
+
22
+ - Zero dependencies (standard library only), Python ≥ 3.9
23
+ - Machine-readable spec: [openapi.json](https://rightos.i-s3.com/openapi.json) · AI-agent docs: [llms-full.txt](https://rightos.i-s3.com/llms-full.txt)
24
+
25
+ > RightOS is not a taxi or ride-hailing service. It does not arrange vehicles, set fares, assign drivers, or broker dispatch.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pip install rightos-sdk
31
+ ```
32
+
33
+ ## Quickstart (60 seconds)
34
+
35
+ ```python
36
+ from rightos import RightOS
37
+
38
+ # 1. Register once (the apiKey is returned EXACTLY ONCE — store it securely)
39
+ pub = RightOS()
40
+ reg = pub.register_organization(name="My Shop", contact_email="you@example.com", plan_id="free")
41
+ api_key = reg["apiKey"]
42
+
43
+ # 2. Operator client
44
+ client = RightOS(api_key=api_key)
45
+
46
+ # 3. Issue a digital QR ticket
47
+ location = client.list_locations()[0]
48
+ issued = client.issue_token(location_id=location["id"], title="Queue ticket")
49
+ print("Hand this to your customer:", issued["walletUrl"])
50
+
51
+ # 4. Verify on arrival (no API key needed)
52
+ outcome = RightOS().verify_token(issued["token"]["id"], issued["verificationCode"])
53
+ print(outcome["result"]) # "success"
54
+
55
+ # 5. Mark as used after service
56
+ client.use_token(issued["token"]["id"])
57
+ ```
58
+
59
+ ## Location policies (Policy Engine)
60
+
61
+ ```python
62
+ policy = client.get_location_policy(location["id"])["policy"]
63
+ # {"transferable": True, "maxTransfers": 3, "defaultValidityMinutes": 720, ...}
64
+
65
+ client.set_location_policy(location["id"], {"transferable": False}) # override
66
+ client.set_location_policy(location["id"], None) # reset to industry preset
67
+ ```
68
+
69
+ ## Error handling
70
+
71
+ ```python
72
+ from rightos import RightOS, RightOSError
73
+
74
+ try:
75
+ RightOS().transfer_token(token_id, code)
76
+ except RightOSError as e:
77
+ if e.code == "policy_transfer_disabled":
78
+ ... # this location forbids transfers
79
+ elif e.status == 429:
80
+ ... # rate limited; wait e.retry_after_sec
81
+ ```
82
+
83
+ Common codes: `missing_api_key` / `invalid_api_key` (401), plan limits (402), `policy_transfer_disabled` / `transfer_limit_reached` (409), `rate_limited` (429).
84
+
85
+ ## Try it against the live demo
86
+
87
+ ```python
88
+ demo = RightOS(api_key="rk_demo_00000000000000000000") # shared demo org
89
+ print(demo.list_locations())
90
+ ```
91
+
92
+ ## License
93
+
94
+ MIT © I-S3 Inc.
@@ -0,0 +1,77 @@
1
+ # rightos-sdk — RightOS Python SDK
2
+
3
+ Official Python SDK for [RightOS](https://rightos.i-s3.com/software/rightos) — privacy-preserving rights verification infrastructure. Issue and verify digital QR tickets ("Right Tokens") for queues, reservations, EV charging, and package pickup, without ever collecting end users' names or phone numbers.
4
+
5
+ - Zero dependencies (standard library only), Python ≥ 3.9
6
+ - Machine-readable spec: [openapi.json](https://rightos.i-s3.com/openapi.json) · AI-agent docs: [llms-full.txt](https://rightos.i-s3.com/llms-full.txt)
7
+
8
+ > RightOS is not a taxi or ride-hailing service. It does not arrange vehicles, set fares, assign drivers, or broker dispatch.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install rightos-sdk
14
+ ```
15
+
16
+ ## Quickstart (60 seconds)
17
+
18
+ ```python
19
+ from rightos import RightOS
20
+
21
+ # 1. Register once (the apiKey is returned EXACTLY ONCE — store it securely)
22
+ pub = RightOS()
23
+ reg = pub.register_organization(name="My Shop", contact_email="you@example.com", plan_id="free")
24
+ api_key = reg["apiKey"]
25
+
26
+ # 2. Operator client
27
+ client = RightOS(api_key=api_key)
28
+
29
+ # 3. Issue a digital QR ticket
30
+ location = client.list_locations()[0]
31
+ issued = client.issue_token(location_id=location["id"], title="Queue ticket")
32
+ print("Hand this to your customer:", issued["walletUrl"])
33
+
34
+ # 4. Verify on arrival (no API key needed)
35
+ outcome = RightOS().verify_token(issued["token"]["id"], issued["verificationCode"])
36
+ print(outcome["result"]) # "success"
37
+
38
+ # 5. Mark as used after service
39
+ client.use_token(issued["token"]["id"])
40
+ ```
41
+
42
+ ## Location policies (Policy Engine)
43
+
44
+ ```python
45
+ policy = client.get_location_policy(location["id"])["policy"]
46
+ # {"transferable": True, "maxTransfers": 3, "defaultValidityMinutes": 720, ...}
47
+
48
+ client.set_location_policy(location["id"], {"transferable": False}) # override
49
+ client.set_location_policy(location["id"], None) # reset to industry preset
50
+ ```
51
+
52
+ ## Error handling
53
+
54
+ ```python
55
+ from rightos import RightOS, RightOSError
56
+
57
+ try:
58
+ RightOS().transfer_token(token_id, code)
59
+ except RightOSError as e:
60
+ if e.code == "policy_transfer_disabled":
61
+ ... # this location forbids transfers
62
+ elif e.status == 429:
63
+ ... # rate limited; wait e.retry_after_sec
64
+ ```
65
+
66
+ Common codes: `missing_api_key` / `invalid_api_key` (401), plan limits (402), `policy_transfer_disabled` / `transfer_limit_reached` (409), `rate_limited` (429).
67
+
68
+ ## Try it against the live demo
69
+
70
+ ```python
71
+ demo = RightOS(api_key="rk_demo_00000000000000000000") # shared demo org
72
+ print(demo.list_locations())
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT © I-S3 Inc.
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "rightos-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for RightOS - privacy-preserving rights verification API (digital QR tickets for queues, reservations, EV charging, package pickup)."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "I-S3 Software Division" }]
12
+ requires-python = ">=3.9"
13
+ keywords = ["rightos", "queue", "digital-ticket", "qr", "verification", "i-s3"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://rightos.i-s3.com/software/rightos/docs"
23
+ Repository = "https://github.com/suomimasuda/rightos-sdk"
24
+ "API Spec" = "https://rightos.i-s3.com/openapi.json"
25
+
26
+ [tool.hatch.build.targets.wheel]
27
+ packages = ["rightos"]
@@ -0,0 +1,226 @@
1
+ """RightOS Python SDK (zero-dependency, urllib-based).
2
+
3
+ RightOS is privacy-preserving rights verification infrastructure:
4
+ digital QR tickets ("Right Tokens") for queues, reservations, EV charging,
5
+ and package pickup. It verifies that a valid right is present - never who
6
+ the person is.
7
+
8
+ API reference: https://rightos.i-s3.com/openapi.json
9
+
10
+ Example:
11
+ >>> from rightos import RightOS
12
+ >>> client = RightOS(api_key="rk_live_...")
13
+ >>> issued = client.issue_token(location_id="loc_...", title="Queue ticket")
14
+ >>> # Hand issued["walletUrl"] (QR page) to your customer.
15
+ >>> outcome = client.verify_token(issued["token"]["id"], issued["verificationCode"])
16
+ >>> outcome["result"]
17
+ 'success'
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import urllib.error
24
+ import urllib.request
25
+ from typing import Any, Optional
26
+
27
+ __all__ = ["RightOS", "RightOSError", "DEFAULT_BASE_URL"]
28
+ __version__ = "0.1.0"
29
+
30
+ DEFAULT_BASE_URL = "https://rightos.i-s3.com"
31
+
32
+
33
+ class RightOSError(Exception):
34
+ """Raised for any non-2xx API response.
35
+
36
+ Attributes:
37
+ status: HTTP status code.
38
+ code: Machine-readable error code (e.g. "invalid_api_key",
39
+ "policy_transfer_disabled", "transfer_limit_reached").
40
+ retry_after_sec: Seconds to wait before retrying (present on 429).
41
+ """
42
+
43
+ def __init__(self, status: int, code: str, retry_after_sec: Optional[int] = None):
44
+ super().__init__(f"RightOS API error {status}: {code}")
45
+ self.status = status
46
+ self.code = code
47
+ self.retry_after_sec = retry_after_sec
48
+
49
+
50
+ class RightOS:
51
+ """RightOS API client.
52
+
53
+ Operator methods require ``api_key``. Public methods (verify_token,
54
+ transfer_token, get_token, get_location_policy, list_plans,
55
+ register_organization) work without a key.
56
+ """
57
+
58
+ def __init__(self, api_key: Optional[str] = None, base_url: str = DEFAULT_BASE_URL, timeout: float = 15.0):
59
+ self.api_key = api_key
60
+ self.base_url = base_url.rstrip("/")
61
+ self.timeout = timeout
62
+
63
+ # ---------- internals ----------
64
+
65
+ def _request(self, method: str, path: str, body: Any = ...) -> Any:
66
+ headers = {"User-Agent": f"rightos-sdk-python/{__version__}"}
67
+ data = None
68
+ if body is not ...:
69
+ headers["Content-Type"] = "application/json"
70
+ data = json.dumps(body).encode("utf-8")
71
+ if self.api_key:
72
+ headers["x-rightos-key"] = self.api_key
73
+ req = urllib.request.Request(
74
+ f"{self.base_url}{path}", data=data, headers=headers, method=method
75
+ )
76
+ try:
77
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
78
+ return json.loads(res.read().decode("utf-8"))
79
+ except urllib.error.HTTPError as e:
80
+ code = "unknown_error"
81
+ try:
82
+ payload = json.loads(e.read().decode("utf-8"))
83
+ code = payload.get("error", code)
84
+ except Exception:
85
+ pass
86
+ retry_after = e.headers.get("Retry-After")
87
+ raise RightOSError(
88
+ e.code, code, int(retry_after) if retry_after else None
89
+ ) from None
90
+
91
+ # ---------- Public endpoints (no API key required) ----------
92
+
93
+ def list_plans(self) -> list[dict]:
94
+ """List pricing plans (globally uniform pricing)."""
95
+ return self._request("GET", "/api/rightos/plans")["plans"]
96
+
97
+ def get_token(self, token_id: str) -> dict:
98
+ """Get a Right Token (never includes the secret hash)."""
99
+ return self._request("GET", f"/api/rightos/tokens/{token_id}")["token"]
100
+
101
+ def verify_token(self, token_id: str, verification_code: str) -> dict:
102
+ """Verify a Right Token by its verificationCode.
103
+
104
+ Returns ``{"result": "success" | "failed" | "expired" | "cancelled"
105
+ | "already_used", "token": {...}}``. Rate limited: 10/min per
106
+ IP+token, 60/min per IP.
107
+ """
108
+ return self._request(
109
+ "POST",
110
+ f"/api/rightos/tokens/{token_id}/verify",
111
+ {"verificationCode": verification_code},
112
+ )
113
+
114
+ def transfer_token(self, token_id: str, current_verification_code: str) -> dict:
115
+ """Transfer a right (re-keying). Only the current holder can transfer.
116
+
117
+ Subject to the location policy - raises RightOSError with code
118
+ "policy_transfer_disabled" or "transfer_limit_reached" (HTTP 409).
119
+ The new verificationCode and walletUrl are returned exactly once.
120
+ """
121
+ return self._request(
122
+ "POST",
123
+ f"/api/rightos/tokens/{token_id}/transfer",
124
+ {"verificationCode": current_verification_code},
125
+ )
126
+
127
+ def get_location_policy(self, location_id: str) -> dict:
128
+ """Get a location's effective policy (public, for transparency)."""
129
+ return self._request("GET", f"/api/rightos/locations/{location_id}/policy")
130
+
131
+ def register_organization(
132
+ self, name: str, contact_email: str, plan_id: str = "free", country: str = "JP"
133
+ ) -> dict:
134
+ """Register an organization.
135
+
136
+ The returned apiKey is shown EXACTLY ONCE - store it securely.
137
+ Rate limited: 5/hour per IP.
138
+ """
139
+ return self._request(
140
+ "POST",
141
+ "/api/rightos/organizations",
142
+ {"name": name, "contactEmail": contact_email, "planId": plan_id, "country": country},
143
+ )
144
+
145
+ # ---------- Operator endpoints (API key required) ----------
146
+
147
+ def list_locations(self) -> list[dict]:
148
+ """List your organization's locations."""
149
+ return self._request("GET", "/api/rightos/locations")["locations"]
150
+
151
+ def create_location(
152
+ self,
153
+ name: str,
154
+ address: str = "",
155
+ type: str = "other",
156
+ timezone: str = "Asia/Tokyo",
157
+ ) -> dict:
158
+ """Create a location. Raises 402 when the plan's location limit is exceeded."""
159
+ return self._request(
160
+ "POST",
161
+ "/api/rightos/locations",
162
+ {"name": name, "address": address, "type": type, "timezone": timezone},
163
+ )["location"]
164
+
165
+ def set_location_policy(self, location_id: str, patch: Optional[dict]) -> dict:
166
+ """Override a location's policy (partial update).
167
+
168
+ Pass ``None`` to reset to the industry preset.
169
+ """
170
+ return self._request(
171
+ "PUT", f"/api/rightos/locations/{location_id}/policy", patch
172
+ )
173
+
174
+ def issue_token(
175
+ self,
176
+ location_id: str,
177
+ title: str,
178
+ description: str = "",
179
+ start_time: Optional[str] = None,
180
+ end_time: Optional[str] = None,
181
+ ) -> dict:
182
+ """Issue a Right Token.
183
+
184
+ The verificationCode and walletUrl are returned exactly once -
185
+ hand the walletUrl (QR page) to the end user. Raises 402 when the
186
+ plan's monthly token limit is exceeded.
187
+ """
188
+ body: dict[str, Any] = {"locationId": location_id, "title": title}
189
+ if description:
190
+ body["description"] = description
191
+ if start_time:
192
+ body["startTime"] = start_time
193
+ if end_time:
194
+ body["endTime"] = end_time
195
+ return self._request("POST", "/api/rightos/tokens/issue", body)
196
+
197
+ def use_token(self, token_id: str) -> dict:
198
+ """Mark a token as used (own organization only)."""
199
+ return self._request("POST", f"/api/rightos/tokens/{token_id}/use")["token"]
200
+
201
+ def cancel_token(self, token_id: str) -> dict:
202
+ """Cancel a token (own organization only)."""
203
+ return self._request("POST", f"/api/rightos/tokens/{token_id}/cancel")["token"]
204
+
205
+ def export_data(self) -> dict:
206
+ """Export all organization data as JSON (no lock-in; contains no secrets)."""
207
+ return self._request("GET", "/api/rightos/export")
208
+
209
+ def rotate_api_key(self) -> str:
210
+ """Re-issue the API key. The old key is invalidated immediately.
211
+
212
+ The new key is returned exactly once. This client switches to the
213
+ new key automatically.
214
+ """
215
+ new_key = self._request("POST", "/api/rightos/organizations/rotate-key")["apiKey"]
216
+ self.api_key = new_key
217
+ return new_key
218
+
219
+ def delete_organization(self, confirm_name: str) -> dict:
220
+ """Permanently delete the organization and all its data (irreversible).
221
+
222
+ ``confirm_name`` must exactly match the organization name.
223
+ """
224
+ return self._request(
225
+ "POST", "/api/rightos/organizations/delete", {"confirm": confirm_name}
226
+ )
@@ -0,0 +1,49 @@
1
+ """Live smoke test for the Python SDK against production.
2
+
3
+ Read-heavy: issues exactly one demo token, verifies it, marks it used.
4
+ """
5
+ import sys
6
+
7
+ from rightos import RightOS, RightOSError
8
+
9
+ demo = RightOS(api_key="rk_demo_00000000000000000000")
10
+ pub = RightOS()
11
+ failures = 0
12
+
13
+
14
+ def check(name, ok, extra=""):
15
+ global failures
16
+ print(f"{'OK ' if ok else 'NG '} {name}{' - ' + str(extra) if extra else ''}")
17
+ if not ok:
18
+ failures += 1
19
+
20
+
21
+ plans = pub.list_plans()
22
+ check("list_plans returns 5 plans", len(plans) == 5, len(plans))
23
+
24
+ policy = pub.get_location_policy("loc_ev")
25
+ check("loc_ev policy is non-transferable", policy["policy"]["transferable"] is False)
26
+
27
+ try:
28
+ RightOS(api_key="rk_live_invalid").list_locations()
29
+ check("invalid key raises RightOSError(401)", False)
30
+ except RightOSError as e:
31
+ check("invalid key raises RightOSError(401)", e.status == 401, e.code)
32
+
33
+ locations = demo.list_locations()
34
+ check("demo list_locations > 0", len(locations) > 0, len(locations))
35
+
36
+ issued = demo.issue_token(location_id=locations[0]["id"], title="SDK live test (py)")
37
+ check("issue_token returns walletUrl", issued["token"]["id"] in issued["walletUrl"])
38
+
39
+ verified = pub.verify_token(issued["token"]["id"], issued["verificationCode"])
40
+ check("verify_token -> success", verified["result"] == "success", verified["result"])
41
+
42
+ used = demo.use_token(issued["token"]["id"])
43
+ check("use_token -> used", used["status"] == "used", used["status"])
44
+
45
+ again = pub.verify_token(issued["token"]["id"], issued["verificationCode"])
46
+ check("re-verify -> already_used", again["result"] == "already_used", again["result"])
47
+
48
+ print("ALL PASS" if failures == 0 else f"{failures} FAILURES")
49
+ sys.exit(0 if failures == 0 else 1)