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.
@@ -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"
@@ -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
File without changes
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ stellavend_api