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.
Files changed (56) hide show
  1. {linkmerce-0.2.0 → linkmerce-0.2.2}/PKG-INFO +3 -2
  2. {linkmerce-0.2.0 → linkmerce-0.2.2}/pyproject.toml +3 -2
  3. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/base.py +9 -42
  4. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/__init__.py +1 -1
  5. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/search.py +1 -1
  6. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/__init__.py +2 -2
  7. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/exposure.py +1 -1
  8. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/catalog.py +24 -1
  9. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/sales.py +35 -11
  10. linkmerce-0.2.0/src/linkmerce/types.py → linkmerce-0.2.2/src/linkmerce/common.py +16 -0
  11. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/base.py +5 -3
  12. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/search.py +21 -2
  13. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/exposure.py +3 -3
  14. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/catalog.py +53 -5
  15. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/sales.py +66 -6
  16. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/datetime.py +93 -2
  17. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/duckdb.py +90 -50
  18. linkmerce-0.2.2/src/linkmerce/utils/pyarrow.py +1136 -0
  19. linkmerce-0.2.0/src/linkmerce/exceptions.py +0 -17
  20. linkmerce-0.2.0/src/linkmerce/extensions/bigquery.py +0 -195
  21. linkmerce-0.2.0/src/linkmerce/extensions/gspread.py +0 -262
  22. linkmerce-0.2.0/src/linkmerce/extensions/pandas.py +0 -114
  23. {linkmerce-0.2.0 → linkmerce-0.2.2}/README.md +0 -0
  24. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/__init__.py +0 -0
  25. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/auth/.DS_Store +0 -0
  26. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/auth/__init__.py +0 -0
  27. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/.DS_Store +0 -0
  28. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/__init__.py +0 -0
  29. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/.DS_Store +0 -0
  30. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/__init__.py +0 -0
  31. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/.DS_Store +0 -0
  32. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/__init__.py +0 -0
  33. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/.DS_Store +0 -0
  34. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/__init__.py +0 -0
  35. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/__init__.py +0 -0
  36. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/collect/tasks.py +0 -0
  37. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/.DS_Store +0 -0
  38. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/__init__.py +0 -0
  39. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/.DS_Store +0 -0
  40. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/__init__.py +0 -0
  41. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/.DS_Store +0 -0
  42. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/__init__.py +0 -0
  43. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/__init__.py +0 -0
  44. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/__init__.py +0 -0
  45. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/.DS_Store +0 -0
  46. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/__init__.py +0 -0
  47. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/.DS_Store +0 -0
  48. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/__init__.py +0 -0
  49. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/.DS_Store +0 -0
  50. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/__init__.py +0 -0
  51. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/cast.py +0 -0
  52. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/graphql.py +0 -0
  53. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/headers.py +0 -0
  54. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/jinja.py +0 -0
  55. {linkmerce-0.2.0 → linkmerce-0.2.2}/src/linkmerce/utils/map.py +0 -0
  56. {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.0
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.0"
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.types import JsonObject, JsonSerialize, ResponseComponent
13
- from linkmerce.types import TaskOption, TaskOptions
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 generate_date_pairs(
530
+ def generate_date_context(
565
531
  self,
566
- start: dt.date | str,
567
- end: dt.date | str,
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
- ) -> tuple[dt.date,dt.date] | list[tuple[dt.date,dt.date]]:
536
+ ) -> list[dict[str,dt.date]] | dict[str,dt.date]:
571
537
  from linkmerce.utils.datetime import date_pairs
572
- pairs = date_pairs(start, end, freq, format)
573
- return pairs[0] if len(pairs) == 1 else pairs
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.types import JsonSerialize
9
+ from linkmerce.common import JsonSerialize
10
10
  _KT = TypeVar("_KT", Hashable)
11
11
  _VT = TypeVar("_VT", Any)
12
12
 
@@ -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.types import JsonObject, TaskOptions
8
+ from linkmerce.common import JsonObject, TaskOptions
9
9
 
10
10
 
11
11
  class _SearchCollector(NaverOpenAPI):
@@ -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.types import JsonSerialize
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.exceptions import AuthenticationError
65
+ from linkmerce.common import AuthenticationError
66
66
  raise AuthenticationError("Authentication failed: cookies are invalid.")
67
67
 
68
68
  def authorize(self):
@@ -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.types import JsonObject, TaskOptions
8
+ from linkmerce.common import JsonObject, TaskOptions
9
9
 
10
10
 
11
11
  class ExposureDiagnosis(SearchAdManager):
@@ -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.types import JsonObject, TaskOptions
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.types import JsonObject, TaskOptions
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
- date_pairs = self.generate_date_pairs(start_date, end_date, freq=date_type[0], format=self.date_format)
35
- return (self.request_each(self._build_and_request)
36
- .partial(date_type=date_type, page_size=page_size).expand(mall_seq=mall_seq, date_pairs=date_pairs, page=page)
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
- date_pairs = self.generate_date_pairs(start_date, end_date, freq=date_type[0], format=self.date_format)
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, date_pairs=date_pairs, page=page)
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
- date_pairs: tuple[dt.date,dt.date],
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(date_pairs[0]),
78
- "endDate": str(date_pairs[1]),
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.exceptions import ParseError
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.types import JsonObject
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.exceptions import RequestError
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.types import JsonObject
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.exceptions import RequestError, UnauthorizedError
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.types import JsonObject
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.exceptions import RequestError
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
- {{ mall_seq }} AS mallPid,
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
- mall_seq = "mallProductId" if mall_seq is None else "TRY_CAST(mallProductId AS INT64)"
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.types import JsonObject
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.exceptions import UnauthorizedError
33
+ from linkmerce.common import UnauthorizedError
34
34
  raise UnauthorizedError("Unauthorized request")
35
35
  else:
36
- from linkmerce.exceptions import RequestError
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
- date_pairs: tuple[dt.date,dt.date] = tuple(),
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(*date_pairs, date_type=date_type)
51
- return self.select(data, self.make_query(mall_seq, date_part)) if data else list()
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)