linkmerce 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {linkmerce-0.2.0 → linkmerce-0.2.2}/PKG-INFO +3 -2
- {linkmerce-0.2.0 → linkmerce-0.2.2}/pyproject.toml +3 -2
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/base.py +9 -42
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/__init__.py +1 -1
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/search.py +1 -1
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/__init__.py +2 -2
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/exposure.py +1 -1
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/catalog.py +24 -1
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/sales.py +35 -11
- linkmerce-0.2.0/src/linkmerce/types.py → linkmerce-0.2.2/src/linkmerce/common.py +16 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/base.py +5 -3
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/search.py +21 -2
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/exposure.py +3 -3
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/catalog.py +53 -5
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/sales.py +66 -6
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/datetime.py +93 -2
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/duckdb.py +90 -50
- linkmerce-0.2.2/src/linkmerce/utils/pyarrow.py +1136 -0
- linkmerce-0.2.0/src/linkmerce/exceptions.py +0 -17
- linkmerce-0.2.0/src/linkmerce/extensions/bigquery.py +0 -195
- linkmerce-0.2.0/src/linkmerce/extensions/gspread.py +0 -262
- linkmerce-0.2.0/src/linkmerce/extensions/pandas.py +0 -114
- {linkmerce-0.2.0 → linkmerce-0.2.2}/README.md +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/auth/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/auth/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/tasks.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/.DS_Store +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/__init__.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/cast.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/graphql.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/headers.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/jinja.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/map.py +0 -0
- {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/tqdm.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: linkmerce
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: E-commerce API integration management
|
|
5
5
|
Requires-Dist: aiohttp>=3.12.14
|
|
6
|
-
Requires-Dist: asyncio>=3.4.3
|
|
7
6
|
Requires-Dist: bs4>=0.0.2
|
|
8
7
|
Requires-Dist: duckdb>=1.3.2
|
|
9
8
|
Requires-Dist: jinja2>=3.1.6
|
|
9
|
+
Requires-Dist: pyarrow>=21.0.0
|
|
10
|
+
Requires-Dist: pytz>=2025.2
|
|
10
11
|
Requires-Dist: requests>=2.32.4
|
|
11
12
|
Requires-Dist: tqdm>=4.67.1
|
|
12
13
|
Requires-Python: >=3.10
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "linkmerce"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "E-commerce API integration management"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"aiohttp>=3.12.14",
|
|
9
|
-
"asyncio>=3.4.3",
|
|
10
9
|
"bs4>=0.0.2",
|
|
11
10
|
"duckdb>=1.3.2",
|
|
12
11
|
"jinja2>=3.1.6",
|
|
12
|
+
"pyarrow>=21.0.0",
|
|
13
|
+
"pytz>=2025.2",
|
|
13
14
|
"requests>=2.32.4",
|
|
14
15
|
"tqdm>=4.67.1",
|
|
15
16
|
]
|
|
@@ -9,8 +9,8 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from typing import Any, Coroutine, Hashable, IO, Literal, TypeVar
|
|
10
10
|
from linkmerce.collect.tasks import RequestLoop, RequestEach
|
|
11
11
|
from linkmerce.collect.tasks import PaginateAll, RequestEachPages
|
|
12
|
-
from linkmerce.
|
|
13
|
-
from linkmerce.
|
|
12
|
+
from linkmerce.common import JsonObject, JsonSerialize, ResponseComponent
|
|
13
|
+
from linkmerce.common import TaskOption, TaskOptions
|
|
14
14
|
_KT = TypeVar("_KT", Hashable)
|
|
15
15
|
_VT = TypeVar("_VT", Any)
|
|
16
16
|
|
|
@@ -221,23 +221,6 @@ class RequestSessionClient(BaseSessionClient):
|
|
|
221
221
|
response = self.request_text(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
222
222
|
return BeautifulSoup(response, features)
|
|
223
223
|
|
|
224
|
-
def request_table(
|
|
225
|
-
self,
|
|
226
|
-
method: str,
|
|
227
|
-
url: str,
|
|
228
|
-
params: dict | list[tuple] | bytes | None = None,
|
|
229
|
-
data: dict | list[tuple] | bytes | IO | None = None,
|
|
230
|
-
json: JsonSerialize | None = None,
|
|
231
|
-
headers: dict[str,str] = None,
|
|
232
|
-
cookies: dict | RequestsCookieJar = None,
|
|
233
|
-
content_type: Literal["excel", "csv", "html", "xml"] | Sequence = "xlsx",
|
|
234
|
-
table_options: dict = dict(),
|
|
235
|
-
**kwargs
|
|
236
|
-
):
|
|
237
|
-
from linkmerce.extensions.pandas import read_table
|
|
238
|
-
response = self.request_content(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
239
|
-
return read_table(response, table_format=content_type, **table_options)
|
|
240
|
-
|
|
241
224
|
def with_session(func):
|
|
242
225
|
@functools.wraps(func)
|
|
243
226
|
def wrapper(self: RequestSessionClient, *args, init_session: bool = False, **kwargs):
|
|
@@ -365,23 +348,6 @@ class AiohttpSessionClient(BaseSessionClient):
|
|
|
365
348
|
response = await self.request_async_text(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
366
349
|
return BeautifulSoup(response, features)
|
|
367
350
|
|
|
368
|
-
async def request_async_table(
|
|
369
|
-
self,
|
|
370
|
-
method: str,
|
|
371
|
-
url: str,
|
|
372
|
-
params: dict | list[tuple] | bytes | None = None,
|
|
373
|
-
data: dict | list[tuple] | bytes | IO | None = None,
|
|
374
|
-
json: JsonSerialize | None = None,
|
|
375
|
-
headers: dict[str,str] = None,
|
|
376
|
-
cookies: dict | LooseCookies = None,
|
|
377
|
-
content_type: Literal["excel", "csv", "html", "xml"] | Sequence = "xlsx",
|
|
378
|
-
table_options: dict = dict(),
|
|
379
|
-
**kwargs
|
|
380
|
-
):
|
|
381
|
-
from linkmerce.extensions.pandas import read_table
|
|
382
|
-
response = await self.request_async_content(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
383
|
-
return read_table(response, table_format=content_type, **table_options)
|
|
384
|
-
|
|
385
351
|
def async_with_session(func):
|
|
386
352
|
@functools.wraps(func)
|
|
387
353
|
async def wrapper(self: AiohttpSessionClient, *args, init_session: bool = False, **kwargs):
|
|
@@ -561,13 +527,14 @@ class Collector(SessionClient, ParserClient, TaskClient, metaclass=ABCMeta):
|
|
|
561
527
|
message[key] = getattr(self, f"get_request_{attr}")(**kwargs)
|
|
562
528
|
return message
|
|
563
529
|
|
|
564
|
-
def
|
|
530
|
+
def generate_date_context(
|
|
565
531
|
self,
|
|
566
|
-
|
|
567
|
-
|
|
532
|
+
start_date: dt.date | str,
|
|
533
|
+
end_date: dt.date | str,
|
|
568
534
|
freq: Literal["D","W","M"] = "D",
|
|
569
535
|
format: str = "%Y-%m-%d",
|
|
570
|
-
) ->
|
|
536
|
+
) -> list[dict[str,dt.date]] | dict[str,dt.date]:
|
|
571
537
|
from linkmerce.utils.datetime import date_pairs
|
|
572
|
-
pairs = date_pairs(
|
|
573
|
-
|
|
538
|
+
pairs = date_pairs(start_date, end_date, freq, format)
|
|
539
|
+
context = list(map(lambda values: dict(zip(["start_date","end_date"], values)), pairs))
|
|
540
|
+
return context[0] if len(context) == 1 else context
|
|
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
8
|
from typing import Any, Callable, Hashable, IO, Literal, TypeVar
|
|
9
|
-
from linkmerce.
|
|
9
|
+
from linkmerce.common import JsonSerialize
|
|
10
10
|
_KT = TypeVar("_KT", Hashable)
|
|
11
11
|
_VT = TypeVar("_VT", Any)
|
|
12
12
|
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from typing import Any, Callable, Hashable, IO, Literal, TypeVar
|
|
10
|
-
from linkmerce.
|
|
10
|
+
from linkmerce.common import JsonSerialize
|
|
11
11
|
_KT = TypeVar("_KT", Hashable)
|
|
12
12
|
_VT = TypeVar("_VT", Any)
|
|
13
13
|
|
|
@@ -62,7 +62,7 @@ class SearchAdManager(Collector):
|
|
|
62
62
|
headers = super().get_request_headers(referer=redirect_url, origin=self.origin)
|
|
63
63
|
response = self.request_text("GET", url, headers=headers)
|
|
64
64
|
if response.strip() != "true":
|
|
65
|
-
from linkmerce.
|
|
65
|
+
from linkmerce.common import AuthenticationError
|
|
66
66
|
raise AuthenticationError("Authentication failed: cookies are invalid.")
|
|
67
67
|
|
|
68
68
|
def authorize(self):
|
|
@@ -6,7 +6,7 @@ from typing import Iterable, TYPE_CHECKING
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from typing import Callable, Coroutine, Literal, Sequence
|
|
8
8
|
from linkmerce.collect.tasks import RequestEachPages
|
|
9
|
-
from linkmerce.
|
|
9
|
+
from linkmerce.common import JsonObject, TaskOptions
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class _CatalogCollector(PartnerCenter):
|
|
@@ -278,3 +278,26 @@ class BrandProduct(_CatalogCollector):
|
|
|
278
278
|
"categoryShapeType", "categoryLeafYn", "productStatusCode", "saleMethodTypeCode"
|
|
279
279
|
]
|
|
280
280
|
}, "totalCount"]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class ProductPrice(BrandProduct):
|
|
284
|
+
object_type = "products"
|
|
285
|
+
products: list[dict] = list()
|
|
286
|
+
|
|
287
|
+
def _build_and_request(self, **json) -> JsonObject:
|
|
288
|
+
message = self.build_request(json=json)
|
|
289
|
+
response = self.request_json(**message)
|
|
290
|
+
self._update_products(response, **json)
|
|
291
|
+
return response
|
|
292
|
+
|
|
293
|
+
async def _build_and_request_async(self, **json) -> JsonObject:
|
|
294
|
+
message = self.build_request(json=json)
|
|
295
|
+
response = await self.request_async_json(**message)
|
|
296
|
+
self._update_products(response, **json)
|
|
297
|
+
return response
|
|
298
|
+
|
|
299
|
+
def _update_products(self, response: JsonObject, **kwargs):
|
|
300
|
+
try:
|
|
301
|
+
self.products += self.parse(response, parser="ProductList", **kwargs)
|
|
302
|
+
except:
|
|
303
|
+
pass
|
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
|
|
6
6
|
if TYPE_CHECKING:
|
|
7
7
|
from typing import Iterable, Literal
|
|
8
|
-
from linkmerce.
|
|
8
|
+
from linkmerce.common import JsonObject, TaskOptions
|
|
9
9
|
import datetime as dt
|
|
10
10
|
|
|
11
11
|
|
|
@@ -31,15 +31,15 @@ class _SalesCollector(PartnerCenter):
|
|
|
31
31
|
page_size: int = 1000,
|
|
32
32
|
**kwargs
|
|
33
33
|
) -> JsonObject:
|
|
34
|
-
|
|
35
|
-
return (self.request_each(self._build_and_request)
|
|
36
|
-
.partial(date_type=date_type, page_size=page_size).expand(mall_seq=mall_seq,
|
|
34
|
+
context = self.generate_date_context(start_date, end_date, freq=date_type[0].upper(), format=self.date_format)
|
|
35
|
+
return (self.request_each(self._build_and_request, context=context)
|
|
36
|
+
.partial(date_type=date_type, page_size=page_size).expand(mall_seq=mall_seq, page=page)
|
|
37
37
|
.parse(**self.update_parser(**kwargs)).concat("auto").run())
|
|
38
38
|
|
|
39
39
|
@PartnerCenter.async_with_session
|
|
40
40
|
async def collect_async(
|
|
41
41
|
self,
|
|
42
|
-
mall_seq: int | str,
|
|
42
|
+
mall_seq: int | str | Iterable[int | str],
|
|
43
43
|
start_date: dt.date | str,
|
|
44
44
|
end_date: dt.date | str,
|
|
45
45
|
date_type: Literal["daily","weekly","monthly"] = "daily",
|
|
@@ -47,9 +47,9 @@ class _SalesCollector(PartnerCenter):
|
|
|
47
47
|
page_size: int = 1000,
|
|
48
48
|
**kwargs
|
|
49
49
|
) -> JsonObject:
|
|
50
|
-
|
|
51
|
-
return await (self.request_each(self._build_and_request_async)
|
|
52
|
-
.partial(date_type=date_type, page_size=page_size).expand(mall_seq=mall_seq,
|
|
50
|
+
context = self.generate_date_context(start_date, end_date, freq=date_type[0].upper(), format=self.date_format)
|
|
51
|
+
return await (self.request_each(self._build_and_request_async, context=context)
|
|
52
|
+
.partial(date_type=date_type, page_size=page_size).expand(mall_seq=mall_seq, page=page)
|
|
53
53
|
.parse(**self.update_parser(**kwargs)).concat("auto").run_async())
|
|
54
54
|
|
|
55
55
|
def _build_and_request(self, **json) -> JsonObject:
|
|
@@ -63,7 +63,8 @@ class _SalesCollector(PartnerCenter):
|
|
|
63
63
|
def get_request_body(
|
|
64
64
|
self,
|
|
65
65
|
mall_seq: int | str,
|
|
66
|
-
|
|
66
|
+
start_date: dt.date,
|
|
67
|
+
end_date: dt.date,
|
|
67
68
|
date_type: Literal["daily","weekly","monthly"] = "daily",
|
|
68
69
|
page: int = 1,
|
|
69
70
|
page_size: int = 1000,
|
|
@@ -74,8 +75,8 @@ class _SalesCollector(PartnerCenter):
|
|
|
74
75
|
"queryRequest": {
|
|
75
76
|
"mallSequence": str(mall_seq),
|
|
76
77
|
"dateType": date_type.capitalize(),
|
|
77
|
-
"startDate": str(
|
|
78
|
-
"endDate": str(
|
|
78
|
+
"startDate": str(start_date),
|
|
79
|
+
"endDate": str(end_date),
|
|
79
80
|
**({"sortBy": "PaymentAmount"} if self.sales_type != "store" else dict()),
|
|
80
81
|
**({"pageable": {"page":int(page), "size":int(page_size)}} if self.sales_type != "store" else dict()),
|
|
81
82
|
}
|
|
@@ -141,3 +142,26 @@ class ProductSales(_SalesCollector):
|
|
|
141
142
|
{"visit": ["click"]},
|
|
142
143
|
{"rest": [{"comparePreWeek": ["isNewlyAdded"]}]},
|
|
143
144
|
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class AggregatedSales(ProductSales):
|
|
148
|
+
sales_type = "product"
|
|
149
|
+
products: list[dict] = list()
|
|
150
|
+
|
|
151
|
+
def _build_and_request(self, **json) -> JsonObject:
|
|
152
|
+
message = self.build_request(json=json)
|
|
153
|
+
response = self.request_json(**message)
|
|
154
|
+
self._update_products(response, **json)
|
|
155
|
+
return response
|
|
156
|
+
|
|
157
|
+
async def _build_and_request_async(self, **json) -> JsonObject:
|
|
158
|
+
message = self.build_request(json=json)
|
|
159
|
+
response = await self.request_async_json(**message)
|
|
160
|
+
self._update_products(response, **json)
|
|
161
|
+
return response
|
|
162
|
+
|
|
163
|
+
def _update_products(self, response: JsonObject, **kwargs):
|
|
164
|
+
try:
|
|
165
|
+
self.products += self.parse(response, parser="ProductList", **kwargs)
|
|
166
|
+
except:
|
|
167
|
+
pass
|
|
@@ -12,3 +12,19 @@ ResponseComponent = Union[int, bytes, str, JsonObject, dict[str,str]]
|
|
|
12
12
|
|
|
13
13
|
TaskOption = Dict[_KT,_VT]
|
|
14
14
|
TaskOptions = Dict[_KT,TaskOption]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AuthenticationError(RuntimeError):
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParseError(ValueError):
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class RequestError(RuntimeError):
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class UnauthorizedError(RuntimeError):
|
|
30
|
+
...
|
|
@@ -21,7 +21,7 @@ class Parser(metaclass=ABCMeta):
|
|
|
21
21
|
raise NotImplementedError("The 'parse' method must be implemented.")
|
|
22
22
|
|
|
23
23
|
def raise_parse_error(self, msg: str):
|
|
24
|
-
from linkmerce.
|
|
24
|
+
from linkmerce.common import ParseError
|
|
25
25
|
raise ParseError(msg)
|
|
26
26
|
|
|
27
27
|
|
|
@@ -118,15 +118,17 @@ class QueryParser(RecordsParser, metaclass=ABCMeta):
|
|
|
118
118
|
self,
|
|
119
119
|
type: Literal["DATE","STRING"] = "DATE",
|
|
120
120
|
format: str | None = "%Y-%m-%d",
|
|
121
|
+
interval: str | int | None = None,
|
|
121
122
|
) -> str:
|
|
122
123
|
from linkmerce.utils.duckdb import curret_date
|
|
123
|
-
return curret_date(type, format)
|
|
124
|
+
return curret_date(type, format, interval)
|
|
124
125
|
|
|
125
126
|
def curret_datetime(
|
|
126
127
|
self,
|
|
127
128
|
type: Literal["DATETIME","STRING"] = "DATETIME",
|
|
128
129
|
format: str | None = "%Y-%m-%d %H:%M:%S",
|
|
130
|
+
interval: str | int | None = None,
|
|
129
131
|
tzinfo: str | None = None,
|
|
130
132
|
) -> str:
|
|
131
133
|
from linkmerce.utils.duckdb import curret_datetime
|
|
132
|
-
return curret_datetime(type, format, tzinfo)
|
|
134
|
+
return curret_datetime(type, format, interval, tzinfo)
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from typing import Literal
|
|
10
|
-
from linkmerce.
|
|
10
|
+
from linkmerce.common import JsonObject
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class _SearchParser(QueryParser):
|
|
@@ -27,7 +27,7 @@ class _SearchParser(QueryParser):
|
|
|
27
27
|
return wrapper
|
|
28
28
|
|
|
29
29
|
def raise_request_error(self, response: dict):
|
|
30
|
-
from linkmerce.
|
|
30
|
+
from linkmerce.common import RequestError
|
|
31
31
|
raise RequestError(response.get("errorMessage") or str())
|
|
32
32
|
|
|
33
33
|
@check_errors
|
|
@@ -194,3 +194,22 @@ class ShoppingRank(_SearchParser):
|
|
|
194
194
|
"""
|
|
195
195
|
created_at = self.curret_datetime()
|
|
196
196
|
return self.render_query(query, keyword=keyword, start=start, created_at=created_at)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ShoppingProduct(_SearchParser):
|
|
200
|
+
content_type = "shop"
|
|
201
|
+
|
|
202
|
+
def make_query(self, keyword: str, start: int, **kwargs) -> str:
|
|
203
|
+
query = """
|
|
204
|
+
SELECT
|
|
205
|
+
productId AS nvMid,
|
|
206
|
+
TRY_CAST(REGEXP_EXTRACT(link, '/products/(\\d+)$', 1) AS INT64) AS mallPid,
|
|
207
|
+
'{{ keyword }}' AS keyword,
|
|
208
|
+
((TRY_CAST(productType AS INT1) + 2) % 3) AS productType,
|
|
209
|
+
(ROW_NUMBER() OVER () + {{ start }}) AS displayRank,
|
|
210
|
+
{{ created_at }} AS createdAt
|
|
211
|
+
FROM {{ table }}
|
|
212
|
+
WHERE productId IS NOT NULL
|
|
213
|
+
"""
|
|
214
|
+
created_at = self.curret_datetime()
|
|
215
|
+
return self.render_query(query, keyword=keyword, start=start, created_at=created_at)
|
|
@@ -6,7 +6,7 @@ import functools
|
|
|
6
6
|
from typing import TYPE_CHECKING
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
|
-
from linkmerce.
|
|
9
|
+
from linkmerce.common import JsonObject
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class ExposureDiagnosis(QueryParser):
|
|
@@ -23,7 +23,7 @@ class ExposureDiagnosis(QueryParser):
|
|
|
23
23
|
return wrapper
|
|
24
24
|
|
|
25
25
|
def raise_request_error(self, response: dict):
|
|
26
|
-
from linkmerce.
|
|
26
|
+
from linkmerce.common import RequestError, UnauthorizedError
|
|
27
27
|
msg = response.get("title") or response.get("message") or str()
|
|
28
28
|
if (msg == "Forbidden") or ("권한이 없습니다." in msg) or ("인증이 만료됐습니다." in msg):
|
|
29
29
|
raise UnauthorizedError(msg)
|
|
@@ -47,7 +47,7 @@ class ExposureDiagnosis(QueryParser):
|
|
|
47
47
|
NULLIF(fmpBrand, '') AS mallName,
|
|
48
48
|
NULLIF(fmpMaker, '') AS makerName,
|
|
49
49
|
imageUrl,
|
|
50
|
-
CAST(COALESCE(lowPrice, mobileLowPrice, NULL) AS INT64) AS salesPrice,
|
|
50
|
+
-- CAST(COALESCE(lowPrice, mobileLowPrice, NULL) AS INT64) AS salesPrice,
|
|
51
51
|
{{ created_at }} AS createdAt
|
|
52
52
|
FROM {{ table }}
|
|
53
53
|
{{ where }}
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from typing import Literal
|
|
10
|
-
from linkmerce.
|
|
10
|
+
from linkmerce.common import JsonObject
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class _CatalogParser(QueryParser):
|
|
@@ -27,7 +27,7 @@ class _CatalogParser(QueryParser):
|
|
|
27
27
|
|
|
28
28
|
def raise_request_error(self, response: dict):
|
|
29
29
|
from linkmerce.utils.map import hier_get
|
|
30
|
-
from linkmerce.
|
|
30
|
+
from linkmerce.common import RequestError
|
|
31
31
|
msg = hier_get(response, ["errors",0,"message"]) or "null"
|
|
32
32
|
raise RequestError(f"An error occurred during the request: {msg}")
|
|
33
33
|
|
|
@@ -77,13 +77,14 @@ class BrandProduct(_CatalogParser):
|
|
|
77
77
|
query = """
|
|
78
78
|
SELECT
|
|
79
79
|
TRY_CAST(id AS INT64) AS nvMid,
|
|
80
|
-
{{
|
|
80
|
+
{{ product_id }} AS mallPid,
|
|
81
81
|
TRY_CAST(catalogId AS INT64) AS catalogId,
|
|
82
82
|
name AS productName,
|
|
83
83
|
TRY_CAST(NULLIF(makerSeq, '0') AS INT64) AS makerId,
|
|
84
84
|
makerName,
|
|
85
85
|
TRY_CAST(brandSeq AS INT64) AS brandId,
|
|
86
86
|
brandName,
|
|
87
|
+
TRY_CAST({{ mall_seq }} AS INT64) AS mallSeq,
|
|
87
88
|
mallName,
|
|
88
89
|
TRY_CAST(categoryId AS INT32) AS categoryId,
|
|
89
90
|
categoryName,
|
|
@@ -101,5 +102,52 @@ class BrandProduct(_CatalogParser):
|
|
|
101
102
|
DATE_TRUNC('SECOND', TRY_CAST(registerDate AS TIMESTAMP)) AS registerDate
|
|
102
103
|
FROM {{ table }};
|
|
103
104
|
"""
|
|
104
|
-
|
|
105
|
-
return self.render_query(query, mall_seq=mall_seq)
|
|
105
|
+
product_id = "mallProductId" if mall_seq is None else "TRY_CAST(mallProductId AS INT64)"
|
|
106
|
+
return self.render_query(query, mall_seq=mall_seq, product_id=product_id)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ProductPrice(BrandProduct):
|
|
110
|
+
object_type = "products"
|
|
111
|
+
|
|
112
|
+
def make_query(self, mall_seq: int | str | None = None, **kwargs) -> str:
|
|
113
|
+
query = """
|
|
114
|
+
SELECT * EXCLUDE (seq)
|
|
115
|
+
FROM (
|
|
116
|
+
SELECT
|
|
117
|
+
{{ product_id }} AS mallPid,
|
|
118
|
+
TRY_CAST({{ mall_seq }} AS INT64) AS mallSeq,
|
|
119
|
+
TRY_CAST(categoryId AS INT32) AS categoryId,
|
|
120
|
+
TRY_CAST(lowestPrice AS INT64) AS salesPrice,
|
|
121
|
+
{{ yesterday }} AS updateDate,
|
|
122
|
+
ROW_NUMBER() OVER (PARTITION BY mallPid) AS seq
|
|
123
|
+
FROM {{ table }}
|
|
124
|
+
) WHERE (mallPid IS NOT NULL) AND (seq = 1);
|
|
125
|
+
"""
|
|
126
|
+
product_id = "mallProductId" if mall_seq is None else "TRY_CAST(mallProductId AS INT64)"
|
|
127
|
+
yesterday = self.curret_date(interval=-1)
|
|
128
|
+
return self.render_query(query, mall_seq=mall_seq, product_id=product_id, yesterday=yesterday)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ProductList(BrandProduct):
|
|
132
|
+
object_type = "products"
|
|
133
|
+
|
|
134
|
+
def make_query(self, mall_seq: int | str | None = None, **kwargs) -> str:
|
|
135
|
+
query = """
|
|
136
|
+
SELECT * EXCLUDE (seq)
|
|
137
|
+
FROM (
|
|
138
|
+
SELECT
|
|
139
|
+
{{ product_id }} AS mallPid,
|
|
140
|
+
TRY_CAST({{ mall_seq }} AS INT64) AS mallSeq,
|
|
141
|
+
TRY_CAST(categoryId AS INT32) AS categoryId,
|
|
142
|
+
TRY_CAST(SPLIT_PART(fullCategoryId, '>', 3) AS INT32) AS categoryId3,
|
|
143
|
+
name AS productName,
|
|
144
|
+
TRY_CAST(lowestPrice AS INT64) AS salesPrice,
|
|
145
|
+
TRY_CAST(registerDate AS DATE) AS registerDate,
|
|
146
|
+
{{ today }} AS updateDate,
|
|
147
|
+
ROW_NUMBER() OVER (PARTITION BY mallPid) AS seq
|
|
148
|
+
FROM {{ table }}
|
|
149
|
+
) WHERE (mallPid IS NOT NULL) AND (seq = 1);
|
|
150
|
+
"""
|
|
151
|
+
product_id = "mallProductId" if mall_seq is None else "TRY_CAST(mallProductId AS INT64)"
|
|
152
|
+
today = self.curret_date()
|
|
153
|
+
return self.render_query(query, mall_seq=mall_seq, product_id=product_id, today=today)
|
|
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from typing import Literal
|
|
10
|
-
from linkmerce.
|
|
10
|
+
from linkmerce.common import JsonObject
|
|
11
11
|
import datetime as dt
|
|
12
12
|
|
|
13
13
|
|
|
@@ -30,10 +30,10 @@ class _SalesParser(QueryParser):
|
|
|
30
30
|
from linkmerce.utils.map import hier_get
|
|
31
31
|
msg = hier_get(response, ["error","error"]) or "null"
|
|
32
32
|
if msg == "Unauthorized":
|
|
33
|
-
from linkmerce.
|
|
33
|
+
from linkmerce.common import UnauthorizedError
|
|
34
34
|
raise UnauthorizedError("Unauthorized request")
|
|
35
35
|
else:
|
|
36
|
-
from linkmerce.
|
|
36
|
+
from linkmerce.common import RequestError
|
|
37
37
|
raise RequestError(f"An error occurred during the request: {msg}")
|
|
38
38
|
|
|
39
39
|
@check_errors
|
|
@@ -41,14 +41,16 @@ class _SalesParser(QueryParser):
|
|
|
41
41
|
self,
|
|
42
42
|
response: JsonObject,
|
|
43
43
|
mall_seq: int | str | None = None,
|
|
44
|
-
|
|
44
|
+
start_date: dt.date | None = None,
|
|
45
|
+
end_date: dt.date | None = None,
|
|
45
46
|
date_type: Literal["daily","weekly","monthly"] = "daily",
|
|
46
47
|
**kwargs
|
|
47
48
|
) -> list[dict]:
|
|
48
49
|
data = response["data"][f"{self.sales_type}Sales"]
|
|
49
50
|
mall_seq = string if (string := str(mall_seq)).isdigit() else "NULL"
|
|
50
|
-
date_part = self.build_date_part(
|
|
51
|
-
|
|
51
|
+
date_part = self.build_date_part(start_date, end_date, date_type)
|
|
52
|
+
kwargs = dict(mall_seq=mall_seq, date_part=date_part, start_date=start_date, end_date=end_date)
|
|
53
|
+
return self.select(data, self.make_query(**kwargs)) if data else list()
|
|
52
54
|
|
|
53
55
|
def build_date_part(self, start_date = None, end_date = None, date_type = "daily") -> str:
|
|
54
56
|
if date_type == "daily":
|
|
@@ -110,3 +112,61 @@ class ProductSales(_SalesParser):
|
|
|
110
112
|
FROM {{ table }};
|
|
111
113
|
"""
|
|
112
114
|
return self.render_query(query, mall_seq=mall_seq, date_part=date_part)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class AggregatedSales(ProductSales):
|
|
118
|
+
object_type = "products"
|
|
119
|
+
|
|
120
|
+
def make_query(self, mall_seq: str, date_part: str, **kwargs) -> str:
|
|
121
|
+
query = """
|
|
122
|
+
WITH product_sales AS (
|
|
123
|
+
SELECT
|
|
124
|
+
TRY_CAST(product.identifier AS INT64) AS mallPid,
|
|
125
|
+
{{ mall_seq }} AS mallSeq,
|
|
126
|
+
TRY_CAST(product.category.identifier AS INT32) AS categoryId3,
|
|
127
|
+
visit.click AS clickCount,
|
|
128
|
+
sales.paymentCount AS paymentCount,
|
|
129
|
+
sales.paymentAmount AS paymentAmount,
|
|
130
|
+
{{ date_part }}
|
|
131
|
+
FROM {{ table }}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
SELECT
|
|
135
|
+
mallPid,
|
|
136
|
+
MAX(mallSeq) AS mallSeq,
|
|
137
|
+
MAX(categoryId3) AS categoryId3,
|
|
138
|
+
SUM(clickCount) AS clickCount,
|
|
139
|
+
SUM(paymentCount) AS paymentCount,
|
|
140
|
+
SUM(paymentAmount) AS paymentAmount,
|
|
141
|
+
{{ date_group }}
|
|
142
|
+
FROM product_sales
|
|
143
|
+
WHERE mallPid IS NOT NULL
|
|
144
|
+
GROUP BY mallPid, {{ date_group }};
|
|
145
|
+
"""
|
|
146
|
+
date_group = "paymentDate" if "paymentDate" in date_part else "startDate, endDate"
|
|
147
|
+
return self.render_query(query, mall_seq=mall_seq, date_part=date_part, date_group=date_group)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class ProductList(ProductSales):
|
|
151
|
+
object_type = "products"
|
|
152
|
+
|
|
153
|
+
def make_query(self, mall_seq: str, start_date: dt.date | None = None, **kwargs) -> str:
|
|
154
|
+
query = """
|
|
155
|
+
SELECT * EXCLUDE (seq)
|
|
156
|
+
FROM (
|
|
157
|
+
SELECT
|
|
158
|
+
TRY_CAST(product.identifier AS INT64) AS mallPid,
|
|
159
|
+
{{ mall_seq }} AS mallSeq,
|
|
160
|
+
NULL AS categoryId,
|
|
161
|
+
TRY_CAST(product.category.identifier AS INT32) AS categoryId3,
|
|
162
|
+
product.name AS productName,
|
|
163
|
+
NULL AS salesPrice,
|
|
164
|
+
{{ start_date }} AS registerDate,
|
|
165
|
+
{{ today }} AS updateDate,
|
|
166
|
+
ROW_NUMBER() OVER (PARTITION BY mallPid) AS seq
|
|
167
|
+
FROM {{ table }}
|
|
168
|
+
) WHERE (mallPid IS NOT NULL) AND (seq = 1);
|
|
169
|
+
"""
|
|
170
|
+
start_date = f"DATE '{start_date}'" if start_date is not None else "NULL"
|
|
171
|
+
today = self.curret_date()
|
|
172
|
+
return self.render_query(query, mall_seq=mall_seq, start_date=start_date, today=today)
|