xache 5.0.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.
- xache/__init__.py +142 -0
- xache/client.py +331 -0
- xache/crypto/__init__.py +17 -0
- xache/crypto/signing.py +244 -0
- xache/crypto/wallet.py +240 -0
- xache/errors.py +184 -0
- xache/payment/__init__.py +5 -0
- xache/payment/handler.py +244 -0
- xache/services/__init__.py +29 -0
- xache/services/budget.py +285 -0
- xache/services/collective.py +174 -0
- xache/services/extraction.py +173 -0
- xache/services/facilitator.py +296 -0
- xache/services/identity.py +415 -0
- xache/services/memory.py +401 -0
- xache/services/owner.py +293 -0
- xache/services/receipts.py +202 -0
- xache/services/reputation.py +274 -0
- xache/services/royalty.py +290 -0
- xache/services/sessions.py +268 -0
- xache/services/workspaces.py +447 -0
- xache/types.py +399 -0
- xache/utils/__init__.py +5 -0
- xache/utils/cache.py +214 -0
- xache/utils/http.py +209 -0
- xache/utils/retry.py +101 -0
- xache-5.0.0.dist-info/METADATA +337 -0
- xache-5.0.0.dist-info/RECORD +30 -0
- xache-5.0.0.dist-info/WHEEL +5 -0
- xache-5.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Receipts Service - Access receipts and Merkle proofs"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Any, Optional
|
|
4
|
+
from ..types import Receipt, ReceiptWithProof, UsageAnalytics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReceiptsService:
|
|
8
|
+
"""Receipt service for transaction records"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client):
|
|
11
|
+
self.client = client
|
|
12
|
+
|
|
13
|
+
async def list(
|
|
14
|
+
self,
|
|
15
|
+
limit: int = 50,
|
|
16
|
+
offset: int = 0,
|
|
17
|
+
) -> Dict[str, Any]:
|
|
18
|
+
"""
|
|
19
|
+
List receipts for authenticated agent (free)
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
```python
|
|
23
|
+
result = await client.receipts.list(limit=20, offset=0)
|
|
24
|
+
for receipt in result["receipts"]:
|
|
25
|
+
print(f"{receipt.operation}: ${receipt.amount_usd}")
|
|
26
|
+
```
|
|
27
|
+
"""
|
|
28
|
+
self._validate_list_options(limit, offset)
|
|
29
|
+
|
|
30
|
+
response = await self.client.request(
|
|
31
|
+
"GET",
|
|
32
|
+
f"/v1/receipts?limit={limit}&offset={offset}",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if not response.success or not response.data:
|
|
36
|
+
raise Exception("Failed to list receipts")
|
|
37
|
+
|
|
38
|
+
data = response.data
|
|
39
|
+
receipts = [Receipt(**r) for r in data["receipts"]]
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
"receipts": receipts,
|
|
43
|
+
"total": data["total"],
|
|
44
|
+
"limit": data["limit"],
|
|
45
|
+
"offset": data["offset"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async def get_proof(self, receipt_id: str) -> ReceiptWithProof:
|
|
49
|
+
"""Get Merkle proof for a receipt (free)"""
|
|
50
|
+
if not receipt_id:
|
|
51
|
+
raise ValueError("receipt_id is required")
|
|
52
|
+
|
|
53
|
+
response = await self.client.request(
|
|
54
|
+
"GET",
|
|
55
|
+
f"/v1/receipts/{receipt_id}/proof",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if not response.success or not response.data:
|
|
59
|
+
raise Exception("Failed to get receipt proof")
|
|
60
|
+
|
|
61
|
+
data = response.data
|
|
62
|
+
return ReceiptWithProof(
|
|
63
|
+
receipt_id=data["receiptId"],
|
|
64
|
+
merkle_proof=data["merkleProof"],
|
|
65
|
+
merkle_root=data["merkleRoot"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def get_analytics(
|
|
69
|
+
self,
|
|
70
|
+
start_date: Optional[str] = None,
|
|
71
|
+
end_date: Optional[str] = None,
|
|
72
|
+
) -> UsageAnalytics:
|
|
73
|
+
"""Get usage analytics (free)"""
|
|
74
|
+
params = []
|
|
75
|
+
if start_date:
|
|
76
|
+
params.append(f"startDate={start_date}")
|
|
77
|
+
if end_date:
|
|
78
|
+
params.append(f"endDate={end_date}")
|
|
79
|
+
|
|
80
|
+
query_string = "&".join(params)
|
|
81
|
+
path = f"/v1/analytics/usage?{query_string}" if query_string else "/v1/analytics/usage"
|
|
82
|
+
|
|
83
|
+
response = await self.client.request("GET", path)
|
|
84
|
+
|
|
85
|
+
if not response.success or not response.data:
|
|
86
|
+
raise Exception("Failed to get usage analytics")
|
|
87
|
+
|
|
88
|
+
data = response.data
|
|
89
|
+
return UsageAnalytics(
|
|
90
|
+
operations=data["operations"],
|
|
91
|
+
total_spent=data["totalSpent"],
|
|
92
|
+
period=data["period"],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def get_by_operation(
|
|
96
|
+
self, operation: str, limit: int = 50
|
|
97
|
+
) -> List[Receipt]:
|
|
98
|
+
"""Get receipts for specific operation type"""
|
|
99
|
+
result = await self.list(limit=100)
|
|
100
|
+
return [r for r in result["receipts"] if r.operation == operation][:limit]
|
|
101
|
+
|
|
102
|
+
def _validate_list_options(self, limit: int, offset: int):
|
|
103
|
+
"""Validate list options"""
|
|
104
|
+
if limit < 1 or limit > 100:
|
|
105
|
+
raise ValueError("limit must be between 1 and 100")
|
|
106
|
+
if offset < 0:
|
|
107
|
+
raise ValueError("offset must be non-negative")
|
|
108
|
+
|
|
109
|
+
# ========== Merkle Anchors ==========
|
|
110
|
+
|
|
111
|
+
async def list_anchors(
|
|
112
|
+
self,
|
|
113
|
+
from_date: Optional[str] = None,
|
|
114
|
+
to_date: Optional[str] = None,
|
|
115
|
+
limit: int = 100,
|
|
116
|
+
) -> Dict[str, Any]:
|
|
117
|
+
"""
|
|
118
|
+
List Merkle root anchors with chain status.
|
|
119
|
+
Shows hourly batches of receipts anchored to blockchain.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
from_date: Start date (ISO format, default: 24 hours ago)
|
|
123
|
+
to_date: End date (ISO format, default: now)
|
|
124
|
+
limit: Maximum anchors to return (default: 100)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Anchor list with chain status
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
```python
|
|
131
|
+
result = await client.receipts.list_anchors(
|
|
132
|
+
from_date="2024-01-01T00:00:00Z",
|
|
133
|
+
to_date="2024-01-31T23:59:59Z",
|
|
134
|
+
limit=50
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
for anchor in result['anchors']:
|
|
138
|
+
print(f"{anchor['hour']}: {anchor['receipt_count']} receipts")
|
|
139
|
+
if anchor['base']:
|
|
140
|
+
print(f" Base TX: {anchor['base']['tx_hash']}")
|
|
141
|
+
if anchor['solana']:
|
|
142
|
+
print(f" Solana TX: {anchor['solana']['tx_hash']}")
|
|
143
|
+
if anchor['dual_anchored']:
|
|
144
|
+
print(" ✓ Dual-anchored")
|
|
145
|
+
```
|
|
146
|
+
"""
|
|
147
|
+
params = []
|
|
148
|
+
if from_date:
|
|
149
|
+
params.append(f"from={from_date}")
|
|
150
|
+
if to_date:
|
|
151
|
+
params.append(f"to={to_date}")
|
|
152
|
+
params.append(f"limit={limit}")
|
|
153
|
+
|
|
154
|
+
query_string = "&".join(params)
|
|
155
|
+
path = f"/v1/anchors?{query_string}"
|
|
156
|
+
|
|
157
|
+
response = await self.client.request("GET", path, skip_auth=True)
|
|
158
|
+
|
|
159
|
+
if not response.success or not response.data:
|
|
160
|
+
raise Exception(
|
|
161
|
+
response.error.get("message", "Failed to list anchors")
|
|
162
|
+
if response.error
|
|
163
|
+
else "Failed to list anchors"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
data = response.data
|
|
167
|
+
return {
|
|
168
|
+
"anchors": [
|
|
169
|
+
{
|
|
170
|
+
"hour": a["hour"],
|
|
171
|
+
"merkle_root": a["merkleRoot"],
|
|
172
|
+
"receipt_count": a["receiptCount"],
|
|
173
|
+
"base": (
|
|
174
|
+
{
|
|
175
|
+
"tx_hash": a["base"]["txHash"],
|
|
176
|
+
"gas_used": a["base"].get("gasUsed"),
|
|
177
|
+
"status": a["base"]["status"],
|
|
178
|
+
"anchored_at": a["base"].get("anchoredAt"),
|
|
179
|
+
}
|
|
180
|
+
if a.get("base")
|
|
181
|
+
else None
|
|
182
|
+
),
|
|
183
|
+
"solana": (
|
|
184
|
+
{
|
|
185
|
+
"tx_hash": a["solana"]["txHash"],
|
|
186
|
+
"gas_used": a["solana"].get("gasUsed"),
|
|
187
|
+
"status": a["solana"]["status"],
|
|
188
|
+
"anchored_at": a["solana"].get("anchoredAt"),
|
|
189
|
+
}
|
|
190
|
+
if a.get("solana")
|
|
191
|
+
else None
|
|
192
|
+
),
|
|
193
|
+
"dual_anchored": a.get("dualAnchored", False),
|
|
194
|
+
}
|
|
195
|
+
for a in data.get("anchors", [])
|
|
196
|
+
],
|
|
197
|
+
"total": data.get("total", 0),
|
|
198
|
+
"period": {
|
|
199
|
+
"from": data["period"]["from"],
|
|
200
|
+
"to": data["period"]["to"],
|
|
201
|
+
},
|
|
202
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Reputation Service - Query reputation scores and domain expertise per HLD §2.2"""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from ..types import ReputationSnapshot, DomainReputation, TopAgent, DID
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReputationService:
|
|
8
|
+
"""Reputation service for reputation tracking and leaderboards"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, client):
|
|
11
|
+
self.client = client
|
|
12
|
+
|
|
13
|
+
async def get_reputation(self, agent_did: Optional[DID] = None) -> ReputationSnapshot:
|
|
14
|
+
"""
|
|
15
|
+
Get current reputation snapshot for the authenticated agent
|
|
16
|
+
Free (no payment required)
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
agent_did: Optional agent DID (defaults to authenticated agent)
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Current reputation snapshot with all scores
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
```python
|
|
26
|
+
reputation = await client.reputation.get_reputation()
|
|
27
|
+
|
|
28
|
+
print(f"Overall Score: {reputation.overall}")
|
|
29
|
+
print(f"Memory Quality: {reputation.memory_quality}")
|
|
30
|
+
print(f"Contribution Success: {reputation.contrib_success}")
|
|
31
|
+
print(f"Economic Value: {reputation.economic_value}")
|
|
32
|
+
```
|
|
33
|
+
"""
|
|
34
|
+
endpoint = f"/v1/reputation/{agent_did}" if agent_did else "/v1/reputation"
|
|
35
|
+
|
|
36
|
+
response = await self.client.request("GET", endpoint)
|
|
37
|
+
|
|
38
|
+
if not response.success or not response.data:
|
|
39
|
+
raise Exception("Failed to get reputation")
|
|
40
|
+
|
|
41
|
+
data = response.data
|
|
42
|
+
return ReputationSnapshot(
|
|
43
|
+
agent_did=data["agentDID"],
|
|
44
|
+
timestamp=data["timestamp"],
|
|
45
|
+
overall=data["overall"],
|
|
46
|
+
memory_quality=data.get("memoryQuality", 0),
|
|
47
|
+
contrib_success=data.get("contribSuccess", 0),
|
|
48
|
+
economic_value=data.get("economicValue", 0),
|
|
49
|
+
network_influence=data.get("networkInfluence", 0),
|
|
50
|
+
reliability=data.get("reliability", 0),
|
|
51
|
+
specialization=data.get("specialization", []),
|
|
52
|
+
weights=data.get("weights", {}),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def get_history(
|
|
56
|
+
self, agent_did: Optional[DID] = None, limit: int = 30
|
|
57
|
+
) -> List[ReputationSnapshot]:
|
|
58
|
+
"""
|
|
59
|
+
Get reputation history for the authenticated agent
|
|
60
|
+
Free (no payment required)
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
agent_did: Optional agent DID (defaults to authenticated agent)
|
|
64
|
+
limit: Number of historical snapshots to retrieve (1-100, default: 30)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
List of historical reputation snapshots
|
|
68
|
+
|
|
69
|
+
Example:
|
|
70
|
+
```python
|
|
71
|
+
history = await client.reputation.get_history(limit=10)
|
|
72
|
+
|
|
73
|
+
print(f"Retrieved {len(history)} historical snapshots")
|
|
74
|
+
for i, snapshot in enumerate(history):
|
|
75
|
+
print(f"{i + 1}. {snapshot.timestamp}: {snapshot.overall}")
|
|
76
|
+
```
|
|
77
|
+
"""
|
|
78
|
+
# Validate limit
|
|
79
|
+
self._validate_limit(limit)
|
|
80
|
+
|
|
81
|
+
endpoint = (
|
|
82
|
+
f"/v1/reputation/{agent_did}/history?limit={limit}"
|
|
83
|
+
if agent_did
|
|
84
|
+
else f"/v1/reputation/history?limit={limit}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
response = await self.client.request("GET", endpoint)
|
|
88
|
+
|
|
89
|
+
if not response.success or not response.data:
|
|
90
|
+
raise Exception("Failed to get reputation history")
|
|
91
|
+
|
|
92
|
+
return [
|
|
93
|
+
ReputationSnapshot(
|
|
94
|
+
agent_did=item["agentDID"],
|
|
95
|
+
timestamp=item["timestamp"],
|
|
96
|
+
overall=item["overall"],
|
|
97
|
+
memory_quality=item.get("memoryQuality", 0),
|
|
98
|
+
contrib_success=item.get("contribSuccess", 0),
|
|
99
|
+
economic_value=item.get("economicValue", 0),
|
|
100
|
+
network_influence=item.get("networkInfluence", 0),
|
|
101
|
+
reliability=item.get("reliability", 0),
|
|
102
|
+
specialization=item.get("specialization", []),
|
|
103
|
+
weights=item.get("weights", {}),
|
|
104
|
+
)
|
|
105
|
+
for item in response.data
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
async def get_top_agents(self, limit: int = 10) -> List[TopAgent]:
|
|
109
|
+
"""
|
|
110
|
+
Get top agents by reputation score (leaderboard)
|
|
111
|
+
Free (no payment required)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
limit: Number of top agents to retrieve (1-100, default: 10)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
List of top agents sorted by reputation score
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
```python
|
|
121
|
+
top_agents = await client.reputation.get_top_agents(10)
|
|
122
|
+
|
|
123
|
+
print("Top 10 Agents:")
|
|
124
|
+
for i, agent in enumerate(top_agents):
|
|
125
|
+
print(f"{i + 1}. {agent.agent_did}")
|
|
126
|
+
print(f" Score: {agent.reputation_score}")
|
|
127
|
+
print(f" Operations: {agent.operation_count}")
|
|
128
|
+
print(f" Earned: {agent.total_earned_usd}")
|
|
129
|
+
```
|
|
130
|
+
"""
|
|
131
|
+
# Validate limit
|
|
132
|
+
self._validate_limit(limit)
|
|
133
|
+
|
|
134
|
+
response = await self.client.request(
|
|
135
|
+
"GET", f"/v1/reputation/leaderboard?limit={limit}", skip_auth=True
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if not response.success or not response.data:
|
|
139
|
+
raise Exception("Failed to get top agents")
|
|
140
|
+
|
|
141
|
+
# API returns {leaderboard: [...], total: N}
|
|
142
|
+
leaderboard = response.data.get("leaderboard", response.data)
|
|
143
|
+
if isinstance(leaderboard, dict):
|
|
144
|
+
leaderboard = leaderboard.get("leaderboard", [])
|
|
145
|
+
|
|
146
|
+
return [
|
|
147
|
+
TopAgent(
|
|
148
|
+
agent_did=item["agentDID"],
|
|
149
|
+
wallet_address=item.get("walletAddress", ""),
|
|
150
|
+
reputation_score=item["reputationScore"],
|
|
151
|
+
operation_count=item.get("operationCount", 0),
|
|
152
|
+
total_earned_usd=item.get("totalEarnedUSD", "0"),
|
|
153
|
+
)
|
|
154
|
+
for item in leaderboard
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
async def get_domain_reputation(
|
|
158
|
+
self, domain: str, agent_did: Optional[DID] = None
|
|
159
|
+
) -> Optional[DomainReputation]:
|
|
160
|
+
"""
|
|
161
|
+
Get domain-specific reputation for an agent
|
|
162
|
+
Free (no payment required)
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
domain: Domain name (e.g., 'javascript', 'python', 'devops')
|
|
166
|
+
agent_did: Optional agent DID (defaults to authenticated agent)
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Domain-specific reputation or None if no reputation in domain
|
|
170
|
+
|
|
171
|
+
Example:
|
|
172
|
+
```python
|
|
173
|
+
python_rep = await client.reputation.get_domain_reputation('python')
|
|
174
|
+
|
|
175
|
+
if python_rep:
|
|
176
|
+
print("Python Domain Reputation:")
|
|
177
|
+
print(f" Score: {python_rep.score}")
|
|
178
|
+
print(f" Contributions: {python_rep.contribution_count}")
|
|
179
|
+
print(f" Success Rate: {python_rep.success_rate}")
|
|
180
|
+
print(f" Total Earned: {python_rep.total_earned_usd}")
|
|
181
|
+
else:
|
|
182
|
+
print("No reputation in Python domain yet")
|
|
183
|
+
```
|
|
184
|
+
"""
|
|
185
|
+
# Validate domain
|
|
186
|
+
self._validate_domain(domain)
|
|
187
|
+
|
|
188
|
+
endpoint = (
|
|
189
|
+
f"/v1/reputation/{agent_did}/domain/{domain}"
|
|
190
|
+
if agent_did
|
|
191
|
+
else f"/v1/reputation/domain/{domain}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
response = await self.client.request("GET", endpoint)
|
|
195
|
+
|
|
196
|
+
if not response.success:
|
|
197
|
+
raise Exception("Failed to get domain reputation")
|
|
198
|
+
|
|
199
|
+
if not response.data:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
data = response.data
|
|
203
|
+
return DomainReputation(
|
|
204
|
+
domain=data["domain"],
|
|
205
|
+
score=data["score"],
|
|
206
|
+
contribution_count=data["contributionCount"],
|
|
207
|
+
success_rate=data["successRate"],
|
|
208
|
+
total_earned_usd=data["totalEarnedUSD"],
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
async def get_all_domain_reputations(
|
|
212
|
+
self, agent_did: Optional[DID] = None
|
|
213
|
+
) -> List[DomainReputation]:
|
|
214
|
+
"""
|
|
215
|
+
Get all domain reputations for an agent
|
|
216
|
+
Free (no payment required)
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
agent_did: Optional agent DID (defaults to authenticated agent)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
List of domain reputations
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
```python
|
|
226
|
+
domains = await client.reputation.get_all_domain_reputations()
|
|
227
|
+
|
|
228
|
+
print("Domain Expertise:")
|
|
229
|
+
for domain in domains:
|
|
230
|
+
print(f"{domain.domain}: {domain.score} ({domain.contribution_count} contributions)")
|
|
231
|
+
```
|
|
232
|
+
"""
|
|
233
|
+
endpoint = (
|
|
234
|
+
f"/v1/reputation/{agent_did}/domains"
|
|
235
|
+
if agent_did
|
|
236
|
+
else "/v1/reputation/domains"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
response = await self.client.request("GET", endpoint)
|
|
240
|
+
|
|
241
|
+
if not response.success or not response.data:
|
|
242
|
+
raise Exception("Failed to get domain reputations")
|
|
243
|
+
|
|
244
|
+
return [
|
|
245
|
+
DomainReputation(
|
|
246
|
+
domain=item["domain"],
|
|
247
|
+
score=item["score"],
|
|
248
|
+
contribution_count=item["contributionCount"],
|
|
249
|
+
success_rate=item["successRate"],
|
|
250
|
+
total_earned_usd=item["totalEarnedUSD"],
|
|
251
|
+
)
|
|
252
|
+
for item in response.data
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
def _validate_limit(self, limit: int):
|
|
256
|
+
"""Validate limit parameter"""
|
|
257
|
+
if not isinstance(limit, int):
|
|
258
|
+
raise ValueError("limit must be an integer")
|
|
259
|
+
if limit < 1 or limit > 100:
|
|
260
|
+
raise ValueError("limit must be between 1 and 100")
|
|
261
|
+
|
|
262
|
+
def _validate_domain(self, domain: str):
|
|
263
|
+
"""Validate domain parameter"""
|
|
264
|
+
if not domain or not isinstance(domain, str):
|
|
265
|
+
raise ValueError("domain is required and must be a string")
|
|
266
|
+
if len(domain) < 2:
|
|
267
|
+
raise ValueError("domain must be at least 2 characters")
|
|
268
|
+
if len(domain) > 50:
|
|
269
|
+
raise ValueError("domain must be at most 50 characters")
|
|
270
|
+
# Domain should only contain lowercase letters, numbers, and hyphens
|
|
271
|
+
if not all(c.islower() or c.isdigit() or c == "-" for c in domain):
|
|
272
|
+
raise ValueError(
|
|
273
|
+
"domain must only contain lowercase letters, numbers, and hyphens"
|
|
274
|
+
)
|