stellavend-api-sdk 1.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.
- stellavend_api/__init__.py +47 -0
- stellavend_api/client.py +534 -0
- stellavend_api/py.typed +0 -0
- stellavend_api/types.py +380 -0
- stellavend_api_sdk-1.0.0.dist-info/METADATA +222 -0
- stellavend_api_sdk-1.0.0.dist-info/RECORD +8 -0
- stellavend_api_sdk-1.0.0.dist-info/WHEEL +5 -0
- stellavend_api_sdk-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StellaVend Open API SDK for Python
|
|
3
|
+
|
|
4
|
+
Open API Key (sk-sv-...) 를 사용하는 일반 API SDK입니다.
|
|
5
|
+
Discord 봇용 SDK는 stellavend-bot-sdk를 사용하세요.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import StellaVendClient, StellaVendError
|
|
9
|
+
from .types import (
|
|
10
|
+
Store,
|
|
11
|
+
Product,
|
|
12
|
+
BulkTier,
|
|
13
|
+
Category,
|
|
14
|
+
Grade,
|
|
15
|
+
Coupon,
|
|
16
|
+
User,
|
|
17
|
+
ChargeRequest,
|
|
18
|
+
Order,
|
|
19
|
+
Review,
|
|
20
|
+
Analytics,
|
|
21
|
+
Settings,
|
|
22
|
+
Webhook,
|
|
23
|
+
StockResult,
|
|
24
|
+
BalanceResult,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"StellaVendClient",
|
|
29
|
+
"StellaVendError",
|
|
30
|
+
"Store",
|
|
31
|
+
"Product",
|
|
32
|
+
"BulkTier",
|
|
33
|
+
"Category",
|
|
34
|
+
"Grade",
|
|
35
|
+
"Coupon",
|
|
36
|
+
"User",
|
|
37
|
+
"ChargeRequest",
|
|
38
|
+
"Order",
|
|
39
|
+
"Review",
|
|
40
|
+
"Analytics",
|
|
41
|
+
"Settings",
|
|
42
|
+
"Webhook",
|
|
43
|
+
"StockResult",
|
|
44
|
+
"BalanceResult",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
__version__ = "1.0.0"
|
stellavend_api/client.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""
|
|
2
|
+
StellaVend Open API Client
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, List, Dict, Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .types import (
|
|
10
|
+
Store,
|
|
11
|
+
Product,
|
|
12
|
+
Category,
|
|
13
|
+
Grade,
|
|
14
|
+
Coupon,
|
|
15
|
+
User,
|
|
16
|
+
ChargeRequest,
|
|
17
|
+
Order,
|
|
18
|
+
Review,
|
|
19
|
+
Analytics,
|
|
20
|
+
Settings,
|
|
21
|
+
Webhook,
|
|
22
|
+
StockResult,
|
|
23
|
+
BalanceResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class StellaVendError(Exception):
|
|
28
|
+
"""StellaVend API Error"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
message: str,
|
|
33
|
+
status: int,
|
|
34
|
+
code: Optional[str] = None,
|
|
35
|
+
response: Optional[Dict[str, Any]] = None,
|
|
36
|
+
):
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.status = status
|
|
39
|
+
self.code = code
|
|
40
|
+
self.response = response
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class StellaVendClient:
|
|
44
|
+
"""
|
|
45
|
+
StellaVend Open API Client
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
api_base: API base URL (e.g., "https://stellavend.com/api/v1/mystore")
|
|
49
|
+
api_key: Open API Key (sk-sv-...)
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, api_base: str, api_key: str):
|
|
53
|
+
self.api_base = api_base.rstrip("/")
|
|
54
|
+
self.api_key = api_key
|
|
55
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
56
|
+
|
|
57
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
58
|
+
if self._session is None or self._session.closed:
|
|
59
|
+
self._session = aiohttp.ClientSession()
|
|
60
|
+
return self._session
|
|
61
|
+
|
|
62
|
+
async def close(self):
|
|
63
|
+
"""Close the HTTP session"""
|
|
64
|
+
if self._session and not self._session.closed:
|
|
65
|
+
await self._session.close()
|
|
66
|
+
|
|
67
|
+
async def __aenter__(self):
|
|
68
|
+
return self
|
|
69
|
+
|
|
70
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
71
|
+
await self.close()
|
|
72
|
+
|
|
73
|
+
async def _request(
|
|
74
|
+
self,
|
|
75
|
+
method: str,
|
|
76
|
+
endpoint: str,
|
|
77
|
+
body: Optional[Dict[str, Any]] = None,
|
|
78
|
+
params: Optional[Dict[str, Any]] = None,
|
|
79
|
+
) -> Dict[str, Any]:
|
|
80
|
+
session = await self._get_session()
|
|
81
|
+
url = f"{self.api_base}{endpoint}"
|
|
82
|
+
headers = {
|
|
83
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
84
|
+
"Content-Type": "application/json",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
query_params = {}
|
|
88
|
+
if params:
|
|
89
|
+
for k, v in params.items():
|
|
90
|
+
if v is not None:
|
|
91
|
+
query_params[k] = str(v)
|
|
92
|
+
|
|
93
|
+
async with session.request(
|
|
94
|
+
method, url, headers=headers, json=body, params=query_params or None
|
|
95
|
+
) as response:
|
|
96
|
+
data = await response.json()
|
|
97
|
+
|
|
98
|
+
if not response.ok or data.get("ok") is False:
|
|
99
|
+
raise StellaVendError(
|
|
100
|
+
data.get("error") or data.get("message") or f"API Error: {response.status}",
|
|
101
|
+
response.status,
|
|
102
|
+
data.get("code"),
|
|
103
|
+
data,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return data
|
|
107
|
+
|
|
108
|
+
# ============ Store ============
|
|
109
|
+
|
|
110
|
+
async def get_store(self) -> Store:
|
|
111
|
+
"""Get store information"""
|
|
112
|
+
data = await self._request("GET", "/")
|
|
113
|
+
return Store.from_dict(data.get("store", {}))
|
|
114
|
+
|
|
115
|
+
# ============ Products ============
|
|
116
|
+
|
|
117
|
+
async def get_products(
|
|
118
|
+
self, *, limit: Optional[int] = None, offset: Optional[int] = None
|
|
119
|
+
) -> List[Product]:
|
|
120
|
+
"""Get list of products"""
|
|
121
|
+
data = await self._request("GET", "/products", params={"limit": limit, "offset": offset})
|
|
122
|
+
return [Product.from_dict(p) for p in data.get("products", [])]
|
|
123
|
+
|
|
124
|
+
async def get_product(self, product_id: str) -> Product:
|
|
125
|
+
"""Get product details"""
|
|
126
|
+
data = await self._request("GET", f"/products/{quote(product_id)}")
|
|
127
|
+
return Product.from_dict(data.get("product", {}))
|
|
128
|
+
|
|
129
|
+
async def create_product(
|
|
130
|
+
self,
|
|
131
|
+
*,
|
|
132
|
+
name: str,
|
|
133
|
+
price_krw: int,
|
|
134
|
+
description: Optional[str] = None,
|
|
135
|
+
detail_text: Optional[str] = None,
|
|
136
|
+
category_id: Optional[str] = None,
|
|
137
|
+
is_active: Optional[bool] = None,
|
|
138
|
+
) -> Product:
|
|
139
|
+
"""Create a new product"""
|
|
140
|
+
body: Dict[str, Any] = {"name": name, "priceKrw": price_krw}
|
|
141
|
+
if description is not None:
|
|
142
|
+
body["description"] = description
|
|
143
|
+
if detail_text is not None:
|
|
144
|
+
body["detailText"] = detail_text
|
|
145
|
+
if category_id is not None:
|
|
146
|
+
body["categoryId"] = category_id
|
|
147
|
+
if is_active is not None:
|
|
148
|
+
body["isActive"] = is_active
|
|
149
|
+
|
|
150
|
+
data = await self._request("POST", "/products", body)
|
|
151
|
+
return Product.from_dict(data.get("product", {}))
|
|
152
|
+
|
|
153
|
+
async def update_product(
|
|
154
|
+
self,
|
|
155
|
+
product_id: str,
|
|
156
|
+
*,
|
|
157
|
+
name: Optional[str] = None,
|
|
158
|
+
price_krw: Optional[int] = None,
|
|
159
|
+
description: Optional[str] = None,
|
|
160
|
+
detail_text: Optional[str] = None,
|
|
161
|
+
category_id: Optional[str] = None,
|
|
162
|
+
is_active: Optional[bool] = None,
|
|
163
|
+
) -> Product:
|
|
164
|
+
"""Update a product"""
|
|
165
|
+
body: Dict[str, Any] = {}
|
|
166
|
+
if name is not None:
|
|
167
|
+
body["name"] = name
|
|
168
|
+
if price_krw is not None:
|
|
169
|
+
body["priceKrw"] = price_krw
|
|
170
|
+
if description is not None:
|
|
171
|
+
body["description"] = description
|
|
172
|
+
if detail_text is not None:
|
|
173
|
+
body["detailText"] = detail_text
|
|
174
|
+
if category_id is not None:
|
|
175
|
+
body["categoryId"] = category_id
|
|
176
|
+
if is_active is not None:
|
|
177
|
+
body["isActive"] = is_active
|
|
178
|
+
|
|
179
|
+
data = await self._request("PATCH", f"/products/{quote(product_id)}", body)
|
|
180
|
+
return Product.from_dict(data.get("product", {}))
|
|
181
|
+
|
|
182
|
+
async def delete_product(self, product_id: str) -> bool:
|
|
183
|
+
"""Delete a product"""
|
|
184
|
+
await self._request("DELETE", f"/products/{quote(product_id)}")
|
|
185
|
+
return True
|
|
186
|
+
|
|
187
|
+
# ============ Stock ============
|
|
188
|
+
|
|
189
|
+
async def get_stock(self, product_id: str) -> StockResult:
|
|
190
|
+
"""Get product stock"""
|
|
191
|
+
data = await self._request("GET", f"/products/{quote(product_id)}/stock")
|
|
192
|
+
return StockResult(lines=data.get("lines", []), count=data.get("count", 0))
|
|
193
|
+
|
|
194
|
+
async def add_stock(self, product_id: str, lines: List[str]) -> Dict[str, int]:
|
|
195
|
+
"""Add stock to a product"""
|
|
196
|
+
data = await self._request("POST", f"/products/{quote(product_id)}/stock", {"lines": lines})
|
|
197
|
+
return {"added": data.get("added", 0), "total": data.get("total", 0)}
|
|
198
|
+
|
|
199
|
+
async def clear_stock(self, product_id: str) -> int:
|
|
200
|
+
"""Clear all stock from a product"""
|
|
201
|
+
data = await self._request("DELETE", f"/products/{quote(product_id)}/stock")
|
|
202
|
+
return data.get("cleared", 0)
|
|
203
|
+
|
|
204
|
+
# ============ Categories ============
|
|
205
|
+
|
|
206
|
+
async def get_categories(self) -> List[Category]:
|
|
207
|
+
"""Get all categories"""
|
|
208
|
+
data = await self._request("GET", "/categories")
|
|
209
|
+
return [Category.from_dict(c) for c in data.get("categories", [])]
|
|
210
|
+
|
|
211
|
+
async def create_category(
|
|
212
|
+
self, *, name: str, description: Optional[str] = None, color: Optional[str] = None
|
|
213
|
+
) -> Category:
|
|
214
|
+
"""Create a new category"""
|
|
215
|
+
body: Dict[str, Any] = {"name": name}
|
|
216
|
+
if description is not None:
|
|
217
|
+
body["description"] = description
|
|
218
|
+
if color is not None:
|
|
219
|
+
body["color"] = color
|
|
220
|
+
|
|
221
|
+
data = await self._request("POST", "/categories", body)
|
|
222
|
+
return Category.from_dict(data.get("category", {}))
|
|
223
|
+
|
|
224
|
+
async def update_category(
|
|
225
|
+
self,
|
|
226
|
+
category_id: str,
|
|
227
|
+
*,
|
|
228
|
+
name: Optional[str] = None,
|
|
229
|
+
description: Optional[str] = None,
|
|
230
|
+
color: Optional[str] = None,
|
|
231
|
+
) -> Category:
|
|
232
|
+
"""Update a category"""
|
|
233
|
+
body: Dict[str, Any] = {}
|
|
234
|
+
if name is not None:
|
|
235
|
+
body["name"] = name
|
|
236
|
+
if description is not None:
|
|
237
|
+
body["description"] = description
|
|
238
|
+
if color is not None:
|
|
239
|
+
body["color"] = color
|
|
240
|
+
|
|
241
|
+
data = await self._request("PATCH", f"/categories/{quote(category_id)}", body)
|
|
242
|
+
return Category.from_dict(data.get("category", {}))
|
|
243
|
+
|
|
244
|
+
async def delete_category(self, category_id: str) -> bool:
|
|
245
|
+
"""Delete a category"""
|
|
246
|
+
await self._request("DELETE", f"/categories/{quote(category_id)}")
|
|
247
|
+
return True
|
|
248
|
+
|
|
249
|
+
# ============ Grades ============
|
|
250
|
+
|
|
251
|
+
async def get_grades(self) -> List[Grade]:
|
|
252
|
+
"""Get all grades"""
|
|
253
|
+
data = await self._request("GET", "/grades")
|
|
254
|
+
return [Grade.from_dict(g) for g in data.get("grades", [])]
|
|
255
|
+
|
|
256
|
+
async def create_grade(
|
|
257
|
+
self,
|
|
258
|
+
*,
|
|
259
|
+
name: str,
|
|
260
|
+
min_spend_krw: int,
|
|
261
|
+
discount_type: Optional[str] = None,
|
|
262
|
+
discount_value: Optional[int] = None,
|
|
263
|
+
color: Optional[str] = None,
|
|
264
|
+
) -> Grade:
|
|
265
|
+
"""Create a new grade"""
|
|
266
|
+
body: Dict[str, Any] = {"name": name, "minSpendKrw": min_spend_krw}
|
|
267
|
+
if discount_type is not None:
|
|
268
|
+
body["discountType"] = discount_type
|
|
269
|
+
if discount_value is not None:
|
|
270
|
+
body["discountValue"] = discount_value
|
|
271
|
+
if color is not None:
|
|
272
|
+
body["color"] = color
|
|
273
|
+
|
|
274
|
+
data = await self._request("POST", "/grades", body)
|
|
275
|
+
return Grade.from_dict(data.get("grade", {}))
|
|
276
|
+
|
|
277
|
+
async def update_grade(
|
|
278
|
+
self,
|
|
279
|
+
grade_id: str,
|
|
280
|
+
*,
|
|
281
|
+
name: Optional[str] = None,
|
|
282
|
+
min_spend_krw: Optional[int] = None,
|
|
283
|
+
discount_type: Optional[str] = None,
|
|
284
|
+
discount_value: Optional[int] = None,
|
|
285
|
+
color: Optional[str] = None,
|
|
286
|
+
) -> Grade:
|
|
287
|
+
"""Update a grade"""
|
|
288
|
+
body: Dict[str, Any] = {}
|
|
289
|
+
if name is not None:
|
|
290
|
+
body["name"] = name
|
|
291
|
+
if min_spend_krw is not None:
|
|
292
|
+
body["minSpendKrw"] = min_spend_krw
|
|
293
|
+
if discount_type is not None:
|
|
294
|
+
body["discountType"] = discount_type
|
|
295
|
+
if discount_value is not None:
|
|
296
|
+
body["discountValue"] = discount_value
|
|
297
|
+
if color is not None:
|
|
298
|
+
body["color"] = color
|
|
299
|
+
|
|
300
|
+
data = await self._request("PATCH", f"/grades/{quote(grade_id)}", body)
|
|
301
|
+
return Grade.from_dict(data.get("grade", {}))
|
|
302
|
+
|
|
303
|
+
async def delete_grade(self, grade_id: str) -> bool:
|
|
304
|
+
"""Delete a grade"""
|
|
305
|
+
await self._request("DELETE", f"/grades/{quote(grade_id)}")
|
|
306
|
+
return True
|
|
307
|
+
|
|
308
|
+
# ============ Coupons ============
|
|
309
|
+
|
|
310
|
+
async def get_coupons(
|
|
311
|
+
self, *, limit: Optional[int] = None, offset: Optional[int] = None
|
|
312
|
+
) -> List[Coupon]:
|
|
313
|
+
"""Get all coupons"""
|
|
314
|
+
data = await self._request("GET", "/coupons", params={"limit": limit, "offset": offset})
|
|
315
|
+
return [Coupon.from_dict(c) for c in data.get("coupons", [])]
|
|
316
|
+
|
|
317
|
+
async def create_coupon(
|
|
318
|
+
self,
|
|
319
|
+
*,
|
|
320
|
+
code: str,
|
|
321
|
+
type: str,
|
|
322
|
+
value: int,
|
|
323
|
+
max_uses: Optional[int] = None,
|
|
324
|
+
per_user_limit: Optional[int] = None,
|
|
325
|
+
starts_at: Optional[str] = None,
|
|
326
|
+
expires_at: Optional[str] = None,
|
|
327
|
+
) -> Coupon:
|
|
328
|
+
"""Create a new coupon"""
|
|
329
|
+
body: Dict[str, Any] = {"code": code, "type": type, "value": value}
|
|
330
|
+
if max_uses is not None:
|
|
331
|
+
body["maxUses"] = max_uses
|
|
332
|
+
if per_user_limit is not None:
|
|
333
|
+
body["perUserLimit"] = per_user_limit
|
|
334
|
+
if starts_at is not None:
|
|
335
|
+
body["startsAt"] = starts_at
|
|
336
|
+
if expires_at is not None:
|
|
337
|
+
body["expiresAt"] = expires_at
|
|
338
|
+
|
|
339
|
+
data = await self._request("POST", "/coupons", body)
|
|
340
|
+
return Coupon.from_dict(data.get("coupon", {}))
|
|
341
|
+
|
|
342
|
+
async def update_coupon(
|
|
343
|
+
self,
|
|
344
|
+
coupon_id: str,
|
|
345
|
+
*,
|
|
346
|
+
is_active: Optional[bool] = None,
|
|
347
|
+
max_uses: Optional[int] = None,
|
|
348
|
+
expires_at: Optional[str] = None,
|
|
349
|
+
) -> Coupon:
|
|
350
|
+
"""Update a coupon"""
|
|
351
|
+
body: Dict[str, Any] = {}
|
|
352
|
+
if is_active is not None:
|
|
353
|
+
body["isActive"] = is_active
|
|
354
|
+
if max_uses is not None:
|
|
355
|
+
body["maxUses"] = max_uses
|
|
356
|
+
if expires_at is not None:
|
|
357
|
+
body["expiresAt"] = expires_at
|
|
358
|
+
|
|
359
|
+
data = await self._request("PATCH", f"/coupons/{quote(coupon_id)}", body)
|
|
360
|
+
return Coupon.from_dict(data.get("coupon", {}))
|
|
361
|
+
|
|
362
|
+
async def delete_coupon(self, coupon_id: str) -> bool:
|
|
363
|
+
"""Delete a coupon"""
|
|
364
|
+
await self._request("DELETE", f"/coupons/{quote(coupon_id)}")
|
|
365
|
+
return True
|
|
366
|
+
|
|
367
|
+
# ============ Users ============
|
|
368
|
+
|
|
369
|
+
async def get_users(
|
|
370
|
+
self, *, limit: Optional[int] = None, offset: Optional[int] = None
|
|
371
|
+
) -> List[User]:
|
|
372
|
+
"""Get all users"""
|
|
373
|
+
data = await self._request("GET", "/users", params={"limit": limit, "offset": offset})
|
|
374
|
+
return [User.from_dict(u) for u in data.get("users", [])]
|
|
375
|
+
|
|
376
|
+
async def get_user(self, user_id: str) -> User:
|
|
377
|
+
"""Get user details"""
|
|
378
|
+
data = await self._request("GET", f"/users/{quote(user_id)}")
|
|
379
|
+
return User.from_dict(data.get("user", {}))
|
|
380
|
+
|
|
381
|
+
async def adjust_balance(
|
|
382
|
+
self, user_id: str, amount: int, reason: Optional[str] = None
|
|
383
|
+
) -> BalanceResult:
|
|
384
|
+
"""Adjust user balance"""
|
|
385
|
+
body: Dict[str, Any] = {"amount": amount}
|
|
386
|
+
if reason is not None:
|
|
387
|
+
body["reason"] = reason
|
|
388
|
+
|
|
389
|
+
data = await self._request("POST", f"/users/{quote(user_id)}/balance", body)
|
|
390
|
+
return BalanceResult.from_dict(data)
|
|
391
|
+
|
|
392
|
+
async def set_grade(self, user_id: str, grade_id: Optional[str]) -> bool:
|
|
393
|
+
"""Set user grade"""
|
|
394
|
+
await self._request("PUT", f"/users/{quote(user_id)}/grade", {"gradeId": grade_id})
|
|
395
|
+
return True
|
|
396
|
+
|
|
397
|
+
# ============ Charges ============
|
|
398
|
+
|
|
399
|
+
async def get_charges(
|
|
400
|
+
self,
|
|
401
|
+
*,
|
|
402
|
+
status: Optional[str] = None,
|
|
403
|
+
limit: Optional[int] = None,
|
|
404
|
+
offset: Optional[int] = None,
|
|
405
|
+
) -> List[ChargeRequest]:
|
|
406
|
+
"""Get charge requests"""
|
|
407
|
+
data = await self._request(
|
|
408
|
+
"GET", "/charges", params={"status": status, "limit": limit, "offset": offset}
|
|
409
|
+
)
|
|
410
|
+
return [ChargeRequest.from_dict(c) for c in data.get("charges", [])]
|
|
411
|
+
|
|
412
|
+
async def approve_charge(self, charge_id: str, approved_krw: Optional[int] = None) -> bool:
|
|
413
|
+
"""Approve a charge request"""
|
|
414
|
+
body: Dict[str, Any] = {}
|
|
415
|
+
if approved_krw is not None:
|
|
416
|
+
body["approvedKrw"] = approved_krw
|
|
417
|
+
|
|
418
|
+
await self._request("POST", f"/charges/{quote(charge_id)}/approve", body)
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
async def reject_charge(self, charge_id: str, reason: Optional[str] = None) -> bool:
|
|
422
|
+
"""Reject a charge request"""
|
|
423
|
+
body: Dict[str, Any] = {}
|
|
424
|
+
if reason is not None:
|
|
425
|
+
body["reason"] = reason
|
|
426
|
+
|
|
427
|
+
await self._request("POST", f"/charges/{quote(charge_id)}/reject", body)
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
# ============ Orders ============
|
|
431
|
+
|
|
432
|
+
async def get_orders(
|
|
433
|
+
self,
|
|
434
|
+
*,
|
|
435
|
+
status: Optional[str] = None,
|
|
436
|
+
user_id: Optional[str] = None,
|
|
437
|
+
limit: Optional[int] = None,
|
|
438
|
+
offset: Optional[int] = None,
|
|
439
|
+
) -> List[Order]:
|
|
440
|
+
"""Get orders"""
|
|
441
|
+
data = await self._request(
|
|
442
|
+
"GET",
|
|
443
|
+
"/orders",
|
|
444
|
+
params={"status": status, "userId": user_id, "limit": limit, "offset": offset},
|
|
445
|
+
)
|
|
446
|
+
return [Order.from_dict(o) for o in data.get("orders", [])]
|
|
447
|
+
|
|
448
|
+
async def get_order(self, order_id: str) -> Order:
|
|
449
|
+
"""Get order details"""
|
|
450
|
+
data = await self._request("GET", f"/orders/{quote(order_id)}")
|
|
451
|
+
return Order.from_dict(data.get("order", {}))
|
|
452
|
+
|
|
453
|
+
# ============ Reviews ============
|
|
454
|
+
|
|
455
|
+
async def get_reviews(
|
|
456
|
+
self,
|
|
457
|
+
*,
|
|
458
|
+
product_id: Optional[str] = None,
|
|
459
|
+
limit: Optional[int] = None,
|
|
460
|
+
offset: Optional[int] = None,
|
|
461
|
+
) -> List[Review]:
|
|
462
|
+
"""Get reviews"""
|
|
463
|
+
data = await self._request(
|
|
464
|
+
"GET",
|
|
465
|
+
"/reviews",
|
|
466
|
+
params={"productId": product_id, "limit": limit, "offset": offset},
|
|
467
|
+
)
|
|
468
|
+
return [Review.from_dict(r) for r in data.get("reviews", [])]
|
|
469
|
+
|
|
470
|
+
async def hide_review(self, review_id: str, hide: bool) -> bool:
|
|
471
|
+
"""Hide or unhide a review"""
|
|
472
|
+
await self._request("PATCH", f"/reviews/{quote(review_id)}", {"isHidden": hide})
|
|
473
|
+
return True
|
|
474
|
+
|
|
475
|
+
async def delete_review(self, review_id: str) -> bool:
|
|
476
|
+
"""Delete a review"""
|
|
477
|
+
await self._request("DELETE", f"/reviews/{quote(review_id)}")
|
|
478
|
+
return True
|
|
479
|
+
|
|
480
|
+
# ============ Analytics ============
|
|
481
|
+
|
|
482
|
+
async def get_analytics(self, range: str = "7d") -> Analytics:
|
|
483
|
+
"""Get analytics data"""
|
|
484
|
+
data = await self._request("GET", "/analytics", params={"range": range})
|
|
485
|
+
return Analytics.from_dict(data)
|
|
486
|
+
|
|
487
|
+
# ============ Settings ============
|
|
488
|
+
|
|
489
|
+
async def get_settings(self) -> Settings:
|
|
490
|
+
"""Get store settings"""
|
|
491
|
+
data = await self._request("GET", "/settings")
|
|
492
|
+
return Settings.from_dict(data.get("settings", {}))
|
|
493
|
+
|
|
494
|
+
async def update_settings(
|
|
495
|
+
self,
|
|
496
|
+
*,
|
|
497
|
+
announcement: Optional[str] = None,
|
|
498
|
+
bank_charge: Optional[bool] = None,
|
|
499
|
+
giftcard_charge: Optional[bool] = None,
|
|
500
|
+
allow_guest: Optional[bool] = None,
|
|
501
|
+
allow_unverified_charge: Optional[bool] = None,
|
|
502
|
+
) -> bool:
|
|
503
|
+
"""Update store settings"""
|
|
504
|
+
body: Dict[str, Any] = {}
|
|
505
|
+
if announcement is not None:
|
|
506
|
+
body["announcement"] = announcement
|
|
507
|
+
if bank_charge is not None:
|
|
508
|
+
body["bankCharge"] = bank_charge
|
|
509
|
+
if giftcard_charge is not None:
|
|
510
|
+
body["giftcardCharge"] = giftcard_charge
|
|
511
|
+
if allow_guest is not None:
|
|
512
|
+
body["allowGuest"] = allow_guest
|
|
513
|
+
if allow_unverified_charge is not None:
|
|
514
|
+
body["allowUnverifiedCharge"] = allow_unverified_charge
|
|
515
|
+
|
|
516
|
+
await self._request("PATCH", "/settings", body)
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
# ============ Webhooks ============
|
|
520
|
+
|
|
521
|
+
async def get_webhooks(self) -> List[Webhook]:
|
|
522
|
+
"""Get all webhooks"""
|
|
523
|
+
data = await self._request("GET", "/webhooks")
|
|
524
|
+
return [Webhook.from_dict(w) for w in data.get("webhooks", [])]
|
|
525
|
+
|
|
526
|
+
async def create_webhook(self, *, url: str, events: List[str]) -> Webhook:
|
|
527
|
+
"""Create a new webhook"""
|
|
528
|
+
data = await self._request("POST", "/webhooks", {"url": url, "events": events})
|
|
529
|
+
return Webhook.from_dict(data.get("webhook", {}))
|
|
530
|
+
|
|
531
|
+
async def delete_webhook(self, webhook_id: str) -> bool:
|
|
532
|
+
"""Delete a webhook"""
|
|
533
|
+
await self._request("DELETE", f"/webhooks/{quote(webhook_id)}")
|
|
534
|
+
return True
|
stellavend_api/py.typed
ADDED
|
File without changes
|
stellavend_api/types.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type definitions for StellaVend Open API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional, List, Dict, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Store:
|
|
11
|
+
id: str
|
|
12
|
+
slug: str
|
|
13
|
+
name: str
|
|
14
|
+
description: Optional[str] = None
|
|
15
|
+
announcement: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Store":
|
|
19
|
+
return cls(
|
|
20
|
+
id=data.get("id", ""),
|
|
21
|
+
slug=data.get("slug", ""),
|
|
22
|
+
name=data.get("name", ""),
|
|
23
|
+
description=data.get("description"),
|
|
24
|
+
announcement=data.get("announcement"),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BulkTier:
|
|
30
|
+
min_qty: int
|
|
31
|
+
price_krw: int
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BulkTier":
|
|
35
|
+
return cls(
|
|
36
|
+
min_qty=data.get("minQty", 0),
|
|
37
|
+
price_krw=data.get("priceKrw", 0),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Product:
|
|
43
|
+
id: str
|
|
44
|
+
name: str
|
|
45
|
+
description: str
|
|
46
|
+
detail_text: str
|
|
47
|
+
price_krw: int
|
|
48
|
+
category_id: Optional[str]
|
|
49
|
+
bulk_tiers: List[BulkTier]
|
|
50
|
+
stock_count: int
|
|
51
|
+
is_active: bool
|
|
52
|
+
created_at: str
|
|
53
|
+
updated_at: str
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Product":
|
|
57
|
+
return cls(
|
|
58
|
+
id=data.get("id", ""),
|
|
59
|
+
name=data.get("name", ""),
|
|
60
|
+
description=data.get("description", ""),
|
|
61
|
+
detail_text=data.get("detailText", ""),
|
|
62
|
+
price_krw=data.get("priceKrw", 0),
|
|
63
|
+
category_id=data.get("categoryId"),
|
|
64
|
+
bulk_tiers=[BulkTier.from_dict(t) for t in data.get("bulkTiers", [])],
|
|
65
|
+
stock_count=data.get("stockCount", 0),
|
|
66
|
+
is_active=data.get("isActive", False),
|
|
67
|
+
created_at=data.get("createdAt", ""),
|
|
68
|
+
updated_at=data.get("updatedAt", ""),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Category:
|
|
74
|
+
id: str
|
|
75
|
+
name: str
|
|
76
|
+
description: str
|
|
77
|
+
color: str
|
|
78
|
+
discount_type: Optional[str]
|
|
79
|
+
discount_value: int
|
|
80
|
+
sort_order: int
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Category":
|
|
84
|
+
return cls(
|
|
85
|
+
id=data.get("id", ""),
|
|
86
|
+
name=data.get("name", ""),
|
|
87
|
+
description=data.get("description", ""),
|
|
88
|
+
color=data.get("color", ""),
|
|
89
|
+
discount_type=data.get("discountType"),
|
|
90
|
+
discount_value=data.get("discountValue", 0),
|
|
91
|
+
sort_order=data.get("sortOrder", 0),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Grade:
|
|
97
|
+
id: str
|
|
98
|
+
name: str
|
|
99
|
+
description: str
|
|
100
|
+
color: str
|
|
101
|
+
min_spend_krw: int
|
|
102
|
+
discount_type: Optional[str]
|
|
103
|
+
discount_value: int
|
|
104
|
+
sort_order: int
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Grade":
|
|
108
|
+
return cls(
|
|
109
|
+
id=data.get("id", ""),
|
|
110
|
+
name=data.get("name", ""),
|
|
111
|
+
description=data.get("description", ""),
|
|
112
|
+
color=data.get("color", ""),
|
|
113
|
+
min_spend_krw=data.get("minSpendKrw", 0),
|
|
114
|
+
discount_type=data.get("discountType"),
|
|
115
|
+
discount_value=data.get("discountValue", 0),
|
|
116
|
+
sort_order=data.get("sortOrder", 0),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class Coupon:
|
|
122
|
+
id: str
|
|
123
|
+
code: str
|
|
124
|
+
type: str
|
|
125
|
+
rule: str
|
|
126
|
+
value: int
|
|
127
|
+
min_order_krw: int
|
|
128
|
+
max_order_krw: int
|
|
129
|
+
min_quantity: int
|
|
130
|
+
max_quantity: int
|
|
131
|
+
max_discount_krw: int
|
|
132
|
+
per_user_limit: int
|
|
133
|
+
max_uses: int
|
|
134
|
+
used_count: int
|
|
135
|
+
allow_guest: bool
|
|
136
|
+
starts_at: Optional[str]
|
|
137
|
+
expires_at: Optional[str]
|
|
138
|
+
is_active: bool
|
|
139
|
+
created_at: str
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Coupon":
|
|
143
|
+
return cls(
|
|
144
|
+
id=data.get("id", ""),
|
|
145
|
+
code=data.get("code", ""),
|
|
146
|
+
type=data.get("type", ""),
|
|
147
|
+
rule=data.get("rule", ""),
|
|
148
|
+
value=data.get("value", 0),
|
|
149
|
+
min_order_krw=data.get("minOrderKrw", 0),
|
|
150
|
+
max_order_krw=data.get("maxOrderKrw", 0),
|
|
151
|
+
min_quantity=data.get("minQuantity", 0),
|
|
152
|
+
max_quantity=data.get("maxQuantity", 0),
|
|
153
|
+
max_discount_krw=data.get("maxDiscountKrw", 0),
|
|
154
|
+
per_user_limit=data.get("perUserLimit", 0),
|
|
155
|
+
max_uses=data.get("maxUses", 0),
|
|
156
|
+
used_count=data.get("usedCount", 0),
|
|
157
|
+
allow_guest=data.get("allowGuest", False),
|
|
158
|
+
starts_at=data.get("startsAt"),
|
|
159
|
+
expires_at=data.get("expiresAt"),
|
|
160
|
+
is_active=data.get("isActive", False),
|
|
161
|
+
created_at=data.get("createdAt", ""),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@dataclass
|
|
166
|
+
class User:
|
|
167
|
+
id: str
|
|
168
|
+
username: str
|
|
169
|
+
email: Optional[str]
|
|
170
|
+
balance_krw: int
|
|
171
|
+
total_spent_krw: int
|
|
172
|
+
grade_id: Optional[str]
|
|
173
|
+
grade_name: Optional[str]
|
|
174
|
+
discord_id: Optional[str]
|
|
175
|
+
created_at: str
|
|
176
|
+
last_login_at: Optional[str]
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, data: Dict[str, Any]) -> "User":
|
|
180
|
+
return cls(
|
|
181
|
+
id=data.get("id", ""),
|
|
182
|
+
username=data.get("username", ""),
|
|
183
|
+
email=data.get("email"),
|
|
184
|
+
balance_krw=data.get("balanceKrw", 0),
|
|
185
|
+
total_spent_krw=data.get("totalSpentKrw", 0),
|
|
186
|
+
grade_id=data.get("gradeId"),
|
|
187
|
+
grade_name=data.get("gradeName"),
|
|
188
|
+
discord_id=data.get("discordId"),
|
|
189
|
+
created_at=data.get("createdAt", ""),
|
|
190
|
+
last_login_at=data.get("lastLoginAt"),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class ChargeRequest:
|
|
196
|
+
id: str
|
|
197
|
+
type: str
|
|
198
|
+
status: str
|
|
199
|
+
requested_krw: int
|
|
200
|
+
approved_krw: Optional[int]
|
|
201
|
+
user_id: str
|
|
202
|
+
username: Optional[str]
|
|
203
|
+
created_at: str
|
|
204
|
+
updated_at: str
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_dict(cls, data: Dict[str, Any]) -> "ChargeRequest":
|
|
208
|
+
return cls(
|
|
209
|
+
id=data.get("id", ""),
|
|
210
|
+
type=data.get("type", ""),
|
|
211
|
+
status=data.get("status", ""),
|
|
212
|
+
requested_krw=data.get("requestedKrw", 0),
|
|
213
|
+
approved_krw=data.get("approvedKrw"),
|
|
214
|
+
user_id=data.get("userId", ""),
|
|
215
|
+
username=data.get("username"),
|
|
216
|
+
created_at=data.get("createdAt", ""),
|
|
217
|
+
updated_at=data.get("updatedAt", ""),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class Order:
|
|
223
|
+
id: str
|
|
224
|
+
product_id: str
|
|
225
|
+
product_name: str
|
|
226
|
+
quantity: int
|
|
227
|
+
unit_price_krw: int
|
|
228
|
+
total_price_krw: int
|
|
229
|
+
discount_krw: int
|
|
230
|
+
status: str
|
|
231
|
+
user_id: Optional[str]
|
|
232
|
+
username: Optional[str]
|
|
233
|
+
guest_identifier: Optional[str]
|
|
234
|
+
created_at: str
|
|
235
|
+
delivered_items: Optional[List[str]] = None
|
|
236
|
+
|
|
237
|
+
@classmethod
|
|
238
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Order":
|
|
239
|
+
return cls(
|
|
240
|
+
id=data.get("id", ""),
|
|
241
|
+
product_id=data.get("productId", ""),
|
|
242
|
+
product_name=data.get("productName", ""),
|
|
243
|
+
quantity=data.get("quantity", 0),
|
|
244
|
+
unit_price_krw=data.get("unitPriceKrw", 0),
|
|
245
|
+
total_price_krw=data.get("totalPriceKrw", 0),
|
|
246
|
+
discount_krw=data.get("discountKrw", 0),
|
|
247
|
+
status=data.get("status", ""),
|
|
248
|
+
user_id=data.get("userId"),
|
|
249
|
+
username=data.get("username"),
|
|
250
|
+
guest_identifier=data.get("guestIdentifier"),
|
|
251
|
+
created_at=data.get("createdAt", ""),
|
|
252
|
+
delivered_items=data.get("deliveredItems"),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class Review:
|
|
258
|
+
id: str
|
|
259
|
+
product_id: str
|
|
260
|
+
product_name: str
|
|
261
|
+
user_id: Optional[str]
|
|
262
|
+
username: Optional[str]
|
|
263
|
+
rating: int
|
|
264
|
+
content: str
|
|
265
|
+
is_hidden: bool
|
|
266
|
+
created_at: str
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Review":
|
|
270
|
+
return cls(
|
|
271
|
+
id=data.get("id", ""),
|
|
272
|
+
product_id=data.get("productId", ""),
|
|
273
|
+
product_name=data.get("productName", ""),
|
|
274
|
+
user_id=data.get("userId"),
|
|
275
|
+
username=data.get("username"),
|
|
276
|
+
rating=data.get("rating", 0),
|
|
277
|
+
content=data.get("content", ""),
|
|
278
|
+
is_hidden=data.get("isHidden", False),
|
|
279
|
+
created_at=data.get("createdAt", ""),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class TopProduct:
|
|
285
|
+
product_id: str
|
|
286
|
+
name: str
|
|
287
|
+
count: int
|
|
288
|
+
revenue: int
|
|
289
|
+
|
|
290
|
+
@classmethod
|
|
291
|
+
def from_dict(cls, data: Dict[str, Any]) -> "TopProduct":
|
|
292
|
+
return cls(
|
|
293
|
+
product_id=data.get("productId", ""),
|
|
294
|
+
name=data.get("name", ""),
|
|
295
|
+
count=data.get("count", 0),
|
|
296
|
+
revenue=data.get("revenue", 0),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@dataclass
|
|
301
|
+
class Analytics:
|
|
302
|
+
range: str
|
|
303
|
+
sales_krw: int
|
|
304
|
+
order_count: int
|
|
305
|
+
new_users: int
|
|
306
|
+
top_products: List[TopProduct]
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Analytics":
|
|
310
|
+
return cls(
|
|
311
|
+
range=data.get("range", ""),
|
|
312
|
+
sales_krw=data.get("salesKrw", 0),
|
|
313
|
+
order_count=data.get("orderCount", 0),
|
|
314
|
+
new_users=data.get("newUsers", 0),
|
|
315
|
+
top_products=[TopProduct.from_dict(p) for p in data.get("topProducts", [])],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass
|
|
320
|
+
class Settings:
|
|
321
|
+
announcement: str
|
|
322
|
+
bank_charge: bool
|
|
323
|
+
giftcard_charge: bool
|
|
324
|
+
allow_guest: bool
|
|
325
|
+
allow_unverified_charge: bool
|
|
326
|
+
discord_webhook_url: str
|
|
327
|
+
discord_log_events: List[str]
|
|
328
|
+
|
|
329
|
+
@classmethod
|
|
330
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Settings":
|
|
331
|
+
return cls(
|
|
332
|
+
announcement=data.get("announcement", ""),
|
|
333
|
+
bank_charge=data.get("bankCharge", False),
|
|
334
|
+
giftcard_charge=data.get("giftcardCharge", False),
|
|
335
|
+
allow_guest=data.get("allowGuest", False),
|
|
336
|
+
allow_unverified_charge=data.get("allowUnverifiedCharge", False),
|
|
337
|
+
discord_webhook_url=data.get("discordWebhookUrl", ""),
|
|
338
|
+
discord_log_events=data.get("discordLogEvents", []),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@dataclass
|
|
343
|
+
class Webhook:
|
|
344
|
+
id: str
|
|
345
|
+
url: str
|
|
346
|
+
events: List[str]
|
|
347
|
+
is_active: bool
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def from_dict(cls, data: Dict[str, Any]) -> "Webhook":
|
|
351
|
+
return cls(
|
|
352
|
+
id=data.get("id", ""),
|
|
353
|
+
url=data.get("url", ""),
|
|
354
|
+
events=data.get("events", []),
|
|
355
|
+
is_active=data.get("isActive", False),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@dataclass
|
|
360
|
+
class StockResult:
|
|
361
|
+
lines: List[str]
|
|
362
|
+
count: int
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StockResult":
|
|
366
|
+
return cls(
|
|
367
|
+
lines=data.get("lines", []),
|
|
368
|
+
count=data.get("count", 0),
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@dataclass
|
|
373
|
+
class BalanceResult:
|
|
374
|
+
new_balance: int
|
|
375
|
+
|
|
376
|
+
@classmethod
|
|
377
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BalanceResult":
|
|
378
|
+
return cls(
|
|
379
|
+
new_balance=data.get("newBalance", 0),
|
|
380
|
+
)
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stellavend-api-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: StellaVend Open API SDK for Python
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://stellavend.com
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
19
|
+
|
|
20
|
+
# StellaVend Open API SDK for Python
|
|
21
|
+
|
|
22
|
+
StellaVend Open API를 사용하기 위한 Python SDK입니다.
|
|
23
|
+
|
|
24
|
+
> Discord 봇용 SDK는 `stellavend-bot-sdk`를 사용하세요.
|
|
25
|
+
|
|
26
|
+
## 설치
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install stellavend-api-sdk
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 사용법
|
|
33
|
+
|
|
34
|
+
### 초기화
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from stellavend_api import StellaVendClient
|
|
38
|
+
|
|
39
|
+
client = StellaVendClient(
|
|
40
|
+
api_base="https://stellavend.com/api/v1/mystore",
|
|
41
|
+
api_key="sk-sv-..." # Open API Key
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 상점 정보
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
store = await client.get_store()
|
|
49
|
+
print(store.name)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 상품 관리
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# 목록 조회
|
|
56
|
+
products = await client.get_products(limit=50)
|
|
57
|
+
|
|
58
|
+
# 상세 조회
|
|
59
|
+
product = await client.get_product("prod_xxx")
|
|
60
|
+
|
|
61
|
+
# 생성
|
|
62
|
+
new_product = await client.create_product(
|
|
63
|
+
name="새 상품",
|
|
64
|
+
price_krw=10000,
|
|
65
|
+
description="상품 설명"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# 수정
|
|
69
|
+
await client.update_product("prod_xxx", price_krw=15000, is_active=True)
|
|
70
|
+
|
|
71
|
+
# 삭제
|
|
72
|
+
await client.delete_product("prod_xxx")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 재고 관리
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# 재고 조회
|
|
79
|
+
stock = await client.get_stock("prod_xxx")
|
|
80
|
+
print(f"{stock.count}개 재고")
|
|
81
|
+
|
|
82
|
+
# 재고 추가
|
|
83
|
+
result = await client.add_stock("prod_xxx", ["item1", "item2", "item3"])
|
|
84
|
+
print(f"{result['added']}개 추가됨, 총 {result['total']}개")
|
|
85
|
+
|
|
86
|
+
# 재고 전체 삭제
|
|
87
|
+
await client.clear_stock("prod_xxx")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 카테고리 관리
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
categories = await client.get_categories()
|
|
94
|
+
await client.create_category(name="새 카테고리", color="#ff0000")
|
|
95
|
+
await client.update_category("cat_xxx", name="수정된 이름")
|
|
96
|
+
await client.delete_category("cat_xxx")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 등급 관리
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
grades = await client.get_grades()
|
|
103
|
+
await client.create_grade(
|
|
104
|
+
name="VIP",
|
|
105
|
+
min_spend_krw=100000,
|
|
106
|
+
discount_type="percent",
|
|
107
|
+
discount_value=10
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 쿠폰 관리
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
coupons = await client.get_coupons()
|
|
115
|
+
await client.create_coupon(
|
|
116
|
+
code="WELCOME10",
|
|
117
|
+
type="percent",
|
|
118
|
+
value=10,
|
|
119
|
+
max_uses=100,
|
|
120
|
+
expires_at="2024-12-31T23:59:59Z"
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### 회원 관리
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# 목록 조회
|
|
128
|
+
users = await client.get_users(limit=100)
|
|
129
|
+
|
|
130
|
+
# 상세 조회
|
|
131
|
+
user = await client.get_user("user_xxx")
|
|
132
|
+
|
|
133
|
+
# 잔액 조정
|
|
134
|
+
await client.adjust_balance("user_xxx", 5000, "이벤트 지급")
|
|
135
|
+
await client.adjust_balance("user_xxx", -1000, "패널티 차감")
|
|
136
|
+
|
|
137
|
+
# 등급 설정
|
|
138
|
+
await client.set_grade("user_xxx", "grade_vip")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 충전 요청 관리
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
charges = await client.get_charges(status="pending")
|
|
145
|
+
await client.approve_charge("charge_xxx", approved_krw=10000)
|
|
146
|
+
await client.reject_charge("charge_xxx", reason="증빙 불충분")
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 주문 조회
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
orders = await client.get_orders(status="completed", limit=50)
|
|
153
|
+
order = await client.get_order("order_xxx")
|
|
154
|
+
print(order.delivered_items)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 후기 관리
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
reviews = await client.get_reviews(product_id="prod_xxx")
|
|
161
|
+
await client.hide_review("review_xxx", True)
|
|
162
|
+
await client.delete_review("review_xxx")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 분석 데이터
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
analytics = await client.get_analytics("7d")
|
|
169
|
+
print(f"매출: {analytics.sales_krw}원, 주문: {analytics.order_count}건")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### 설정
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
settings = await client.get_settings()
|
|
176
|
+
await client.update_settings(
|
|
177
|
+
announcement="새 공지사항",
|
|
178
|
+
bank_charge=True
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 웹훅
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
webhooks = await client.get_webhooks()
|
|
186
|
+
await client.create_webhook(
|
|
187
|
+
url="https://example.com/webhook",
|
|
188
|
+
events=["order.completed", "charge.approved"]
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Context Manager 사용
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
async with StellaVendClient(api_base, api_key) as client:
|
|
196
|
+
products = await client.get_products()
|
|
197
|
+
# 자동으로 세션 정리됨
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 에러 처리
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from stellavend_api import StellaVendClient, StellaVendError
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
await client.get_product("invalid_id")
|
|
207
|
+
except StellaVendError as e:
|
|
208
|
+
print(f"API 오류 ({e.status}): {e}")
|
|
209
|
+
print(f"에러 코드: {e.code}")
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## 타입
|
|
213
|
+
|
|
214
|
+
모든 응답은 dataclass로 정의되어 있습니다:
|
|
215
|
+
|
|
216
|
+
- `Store`, `Product`, `Category`, `Grade`, `Coupon`
|
|
217
|
+
- `User`, `ChargeRequest`, `Order`, `Review`
|
|
218
|
+
- `Analytics`, `Settings`, `Webhook`
|
|
219
|
+
|
|
220
|
+
## 라이선스
|
|
221
|
+
|
|
222
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
stellavend_api/__init__.py,sha256=iB6QTzEEd8DDN_WFJiVyNxTgJlR2lTQwZ2U1vFC-nWk,775
|
|
2
|
+
stellavend_api/client.py,sha256=kJyqE0GR-OsgYdTMGPrBtsg3r0Rmesn-4ZFiDMeKjpg,18133
|
|
3
|
+
stellavend_api/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
stellavend_api/types.py,sha256=wECeQ7LiIbpXrJLxJlNOXo0DhdoSczwud1jk9aUlPKo,10317
|
|
5
|
+
stellavend_api_sdk-1.0.0.dist-info/METADATA,sha256=50xp2_SBKcaVGrEfiePcDIl_gpX1j89N6ivQL7JMHj8,4954
|
|
6
|
+
stellavend_api_sdk-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
7
|
+
stellavend_api_sdk-1.0.0.dist-info/top_level.txt,sha256=g-ZG_NH--qWKos7-XfAScF8OzfrH0EZRLxxWA655odU,15
|
|
8
|
+
stellavend_api_sdk-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stellavend_api
|