linkmerce 0.2.2__tar.gz → 0.3.1__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 (79) hide show
  1. {linkmerce-0.2.2 → linkmerce-0.3.1}/PKG-INFO +2 -3
  2. {linkmerce-0.2.2 → linkmerce-0.3.1}/pyproject.toml +2 -3
  3. linkmerce-0.3.1/src/linkmerce/.DS_Store +0 -0
  4. {linkmerce-0.2.2/src/linkmerce/auth → linkmerce-0.3.1/src/linkmerce/api}/__init__.py +0 -0
  5. {linkmerce-0.2.2/src/linkmerce/collect → linkmerce-0.3.1/src/linkmerce/api}/naver/__init__.py +0 -0
  6. linkmerce-0.3.1/src/linkmerce/api/naver/openapi.py +176 -0
  7. {linkmerce-0.2.2/src/linkmerce/collect → linkmerce-0.3.1/src/linkmerce/api}/searchad/__init__.py +0 -0
  8. linkmerce-0.3.1/src/linkmerce/api/searchad/manage.py +53 -0
  9. {linkmerce-0.2.2/src/linkmerce/collect → linkmerce-0.3.1/src/linkmerce/api}/smartstore/__init__.py +0 -0
  10. linkmerce-0.3.1/src/linkmerce/api/smartstore/brand.py +162 -0
  11. linkmerce-0.3.1/src/linkmerce/common/.DS_Store +0 -0
  12. linkmerce-0.3.1/src/linkmerce/common/api.py +186 -0
  13. linkmerce-0.3.1/src/linkmerce/common/exceptions.py +17 -0
  14. linkmerce-0.3.1/src/linkmerce/common/extract.py +440 -0
  15. linkmerce-0.3.1/src/linkmerce/common/load.py +598 -0
  16. linkmerce-0.3.1/src/linkmerce/common/models.py +83 -0
  17. {linkmerce-0.2.2/src/linkmerce/collect → linkmerce-0.3.1/src/linkmerce/common}/tasks.py +154 -121
  18. linkmerce-0.3.1/src/linkmerce/common/transform.py +425 -0
  19. {linkmerce-0.2.2/src/linkmerce/collect → linkmerce-0.3.1/src/linkmerce/core}/.DS_Store +0 -0
  20. {linkmerce-0.2.2/src/linkmerce/collect/smartstore → linkmerce-0.3.1/src/linkmerce/core/naver}/.DS_Store +0 -0
  21. {linkmerce-0.2.2/src/linkmerce/auth → linkmerce-0.3.1/src/linkmerce/core/naver/openapi}/.DS_Store +0 -0
  22. linkmerce-0.3.1/src/linkmerce/core/naver/openapi/__init__.py +1 -0
  23. linkmerce-0.3.1/src/linkmerce/core/naver/openapi/common.py +34 -0
  24. linkmerce-0.2.2/src/linkmerce/collect/naver/api/search.py → linkmerce-0.3.1/src/linkmerce/core/naver/openapi/search/extract.py +45 -57
  25. linkmerce-0.3.1/src/linkmerce/core/naver/openapi/search/models.sql +266 -0
  26. linkmerce-0.3.1/src/linkmerce/core/naver/openapi/search/transform.py +89 -0
  27. {linkmerce-0.2.2/src/linkmerce/collect/naver → linkmerce-0.3.1/src/linkmerce/core/searchad}/.DS_Store +0 -0
  28. {linkmerce-0.2.2/src/linkmerce/collect/naver/api → linkmerce-0.3.1/src/linkmerce/core/searchad/manage}/.DS_Store +0 -0
  29. linkmerce-0.3.1/src/linkmerce/core/searchad/manage/__init__.py +1 -0
  30. linkmerce-0.2.2/src/linkmerce/collect/searchad/manage/__init__.py → linkmerce-0.3.1/src/linkmerce/core/searchad/manage/common.py +24 -35
  31. {linkmerce-0.2.2/src/linkmerce/parse/smartstore/hcenter → linkmerce-0.3.1/src/linkmerce/core/searchad/manage/exposure}/__init__.py +0 -0
  32. linkmerce-0.2.2/src/linkmerce/collect/searchad/manage/exposure.py → linkmerce-0.3.1/src/linkmerce/core/searchad/manage/exposure/extract.py +15 -20
  33. linkmerce-0.3.1/src/linkmerce/core/searchad/manage/exposure/models.sql +108 -0
  34. linkmerce-0.3.1/src/linkmerce/core/searchad/manage/exposure/transform.py +59 -0
  35. linkmerce-0.3.1/src/linkmerce/core/smartstore/.DS_Store +0 -0
  36. {linkmerce-0.2.2/src/linkmerce/parse/smartstore → linkmerce-0.3.1/src/linkmerce/core/smartstore/brand}/.DS_Store +0 -0
  37. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/__init__.py +1 -0
  38. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/catalog/__init__.py +0 -0
  39. linkmerce-0.2.2/src/linkmerce/collect/smartstore/hcenter/catalog.py → linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/catalog/extract.py +113 -92
  40. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/catalog/models.sql +176 -0
  41. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/catalog/transform.py +64 -0
  42. linkmerce-0.2.2/src/linkmerce/collect/smartstore/hcenter/__init__.py → linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/common.py +2 -2
  43. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/sales/__init__.py +0 -0
  44. linkmerce-0.2.2/src/linkmerce/collect/smartstore/hcenter/sales.py → linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/sales/extract.py +19 -45
  45. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/sales/models.sql +164 -0
  46. linkmerce-0.3.1/src/linkmerce/core/smartstore/brand/sales/transform.py +91 -0
  47. linkmerce-0.3.1/src/linkmerce/utils/__init__.py +0 -0
  48. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/headers.py +1 -1
  49. linkmerce-0.2.2/src/linkmerce/collect/__init__.py +0 -1
  50. linkmerce-0.2.2/src/linkmerce/collect/base.py +0 -540
  51. linkmerce-0.2.2/src/linkmerce/collect/naver/api/__init__.py +0 -48
  52. linkmerce-0.2.2/src/linkmerce/common.py +0 -30
  53. linkmerce-0.2.2/src/linkmerce/parse/.DS_Store +0 -0
  54. linkmerce-0.2.2/src/linkmerce/parse/__init__.py +0 -1
  55. linkmerce-0.2.2/src/linkmerce/parse/base.py +0 -134
  56. linkmerce-0.2.2/src/linkmerce/parse/naver/.DS_Store +0 -0
  57. linkmerce-0.2.2/src/linkmerce/parse/naver/api/.DS_Store +0 -0
  58. linkmerce-0.2.2/src/linkmerce/parse/naver/api/search.py +0 -215
  59. linkmerce-0.2.2/src/linkmerce/parse/searchad/manage/exposure.py +0 -77
  60. linkmerce-0.2.2/src/linkmerce/parse/smartstore/hcenter/.DS_Store +0 -0
  61. linkmerce-0.2.2/src/linkmerce/parse/smartstore/hcenter/catalog.py +0 -153
  62. linkmerce-0.2.2/src/linkmerce/parse/smartstore/hcenter/sales.py +0 -172
  63. linkmerce-0.2.2/src/linkmerce/utils/duckdb.py +0 -293
  64. linkmerce-0.2.2/src/linkmerce/utils/pyarrow.py +0 -1136
  65. {linkmerce-0.2.2 → linkmerce-0.3.1}/README.md +0 -0
  66. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/__init__.py +0 -0
  67. {linkmerce-0.2.2/src/linkmerce/parse/naver → linkmerce-0.3.1/src/linkmerce/common}/__init__.py +0 -0
  68. {linkmerce-0.2.2/src/linkmerce/parse/naver/api → linkmerce-0.3.1/src/linkmerce/core}/__init__.py +0 -0
  69. {linkmerce-0.2.2/src/linkmerce/parse/searchad → linkmerce-0.3.1/src/linkmerce/core/naver}/__init__.py +0 -0
  70. {linkmerce-0.2.2/src/linkmerce/parse/searchad/manage → linkmerce-0.3.1/src/linkmerce/core/naver/openapi/search}/__init__.py +0 -0
  71. {linkmerce-0.2.2/src/linkmerce/parse/smartstore → linkmerce-0.3.1/src/linkmerce/core/searchad}/__init__.py +0 -0
  72. {linkmerce-0.2.2/src/linkmerce/utils → linkmerce-0.3.1/src/linkmerce/core/smartstore}/__init__.py +0 -0
  73. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/.DS_Store +0 -0
  74. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/cast.py +0 -0
  75. /linkmerce-0.2.2/src/linkmerce/utils/datetime.py → /linkmerce-0.3.1/src/linkmerce/utils/date.py +0 -0
  76. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/graphql.py +0 -0
  77. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/jinja.py +0 -0
  78. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/map.py +0 -0
  79. {linkmerce-0.2.2 → linkmerce-0.3.1}/src/linkmerce/utils/tqdm.py +0 -0
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: linkmerce
3
- Version: 0.2.2
3
+ Version: 0.3.1
4
4
  Summary: E-commerce API integration management
5
- Requires-Dist: aiohttp>=3.12.14
5
+ Requires-Dist: aiohttp>=3.12.15
6
6
  Requires-Dist: bs4>=0.0.2
7
7
  Requires-Dist: duckdb>=1.3.2
8
8
  Requires-Dist: jinja2>=3.1.6
9
- Requires-Dist: pyarrow>=21.0.0
10
9
  Requires-Dist: pytz>=2025.2
11
10
  Requires-Dist: requests>=2.32.4
12
11
  Requires-Dist: tqdm>=4.67.1
@@ -1,15 +1,14 @@
1
1
  [project]
2
2
  name = "linkmerce"
3
- version = "0.2.2"
3
+ version = "0.3.1"
4
4
  description = "E-commerce API integration management"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
7
7
  dependencies = [
8
- "aiohttp>=3.12.14",
8
+ "aiohttp>=3.12.15",
9
9
  "bs4>=0.0.2",
10
10
  "duckdb>=1.3.2",
11
11
  "jinja2>=3.1.6",
12
- "pyarrow>=21.0.0",
13
12
  "pytz>=2025.2",
14
13
  "requests>=2.32.4",
15
14
  "tqdm>=4.67.1",
Binary file
@@ -0,0 +1,176 @@
1
+ from __future__ import annotations
2
+
3
+ from linkmerce.common.api import run_with_duckdb
4
+ from linkmerce.common.api import get_table_from_options as get_table
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Iterable, Literal
10
+ from linkmerce.common.extract import JsonObject
11
+ from linkmerce.common.load import DuckDBConnection
12
+
13
+
14
+ def get_module(name: str) -> str:
15
+ return (".naver.openapi" + name) if name.startswith('.') else name
16
+
17
+
18
+ def search_blog(
19
+ client_id: str,
20
+ client_secret: str,
21
+ query: str | Iterable[str],
22
+ start: int | Iterable[int] = 1,
23
+ display: int = 100,
24
+ sort: Literal["sim","date"] = "sim",
25
+ connection: DuckDBConnection | None = None,
26
+ how: Literal["sync","async","async_loop"] = "sync",
27
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
28
+ extract_options: dict | None = None,
29
+ transform_options: dict | None = None,
30
+ ) -> JsonObject:
31
+ args = (query, start, display, sort)
32
+ table = get_table(transform_options, "table")
33
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
34
+ options = dict(extract_options=extract_options, transform_options=transform_options)
35
+ return run_with_duckdb(get_module(".search"), "BlogSearch", "BlogSearch", connection, how, table, return_type, args, **options)
36
+
37
+
38
+ def search_news(
39
+ client_id: str,
40
+ client_secret: str,
41
+ query: str | Iterable[str],
42
+ start: int | Iterable[int] = 1,
43
+ display: int = 100,
44
+ sort: Literal["sim","date"] = "sim",
45
+ connection: DuckDBConnection | None = None,
46
+ how: Literal["sync","async","async_loop"] = "sync",
47
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
48
+ extract_options: dict | None = None,
49
+ transform_options: dict | None = None,
50
+ ) -> JsonObject:
51
+ args = (query, start, display, sort)
52
+ table = get_table(transform_options, "table")
53
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
54
+ options = dict(extract_options=extract_options, transform_options=transform_options)
55
+ return run_with_duckdb(get_module(".search"), "NewsSearch", "NewsSearch", connection, how, table, return_type, args, **options)
56
+
57
+
58
+ def search_book(
59
+ client_id: str,
60
+ client_secret: str,
61
+ query: str | Iterable[str],
62
+ start: int | Iterable[int] = 1,
63
+ display: int = 100,
64
+ sort: Literal["sim","date"] = "sim",
65
+ connection: DuckDBConnection | None = None,
66
+ how: Literal["sync","async","async_loop"] = "sync",
67
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
68
+ extract_options: dict | None = None,
69
+ transform_options: dict | None = None,
70
+ ) -> JsonObject:
71
+ args = (query, start, display, sort)
72
+ table = get_table(transform_options, "table")
73
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
74
+ options = dict(extract_options=extract_options, transform_options=transform_options)
75
+ return run_with_duckdb(get_module(".search"), "BookSearch", "BookSearch", connection, how, table, return_type, args, **options)
76
+
77
+
78
+ def search_cafe(
79
+ client_id: str,
80
+ client_secret: str,
81
+ query: str | Iterable[str],
82
+ start: int | Iterable[int] = 1,
83
+ display: int = 100,
84
+ sort: Literal["sim","date"] = "sim",
85
+ connection: DuckDBConnection | None = None,
86
+ how: Literal["sync","async","async_loop"] = "sync",
87
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
88
+ extract_options: dict | None = None,
89
+ transform_options: dict | None = None,
90
+ ) -> JsonObject:
91
+ args = (query, start, display, sort)
92
+ table = get_table(transform_options, "table")
93
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
94
+ options = dict(extract_options=extract_options, transform_options=transform_options)
95
+ return run_with_duckdb(get_module(".search"), "CafeSearch", "CafeSearch", connection, how, table, return_type, args, **options)
96
+
97
+
98
+ def search_kin(
99
+ client_id: str,
100
+ client_secret: str,
101
+ query: str | Iterable[str],
102
+ start: int | Iterable[int] = 1,
103
+ display: int = 100,
104
+ sort: Literal["sim","date","point"] = "sim",
105
+ connection: DuckDBConnection | None = None,
106
+ how: Literal["sync","async","async_loop"] = "sync",
107
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
108
+ extract_options: dict | None = None,
109
+ transform_options: dict | None = None,
110
+ ) -> JsonObject:
111
+ args = (query, start, display, sort)
112
+ table = get_table(transform_options, "table")
113
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
114
+ options = dict(extract_options=extract_options, transform_options=transform_options)
115
+ return run_with_duckdb(get_module(".search"), "KiNSearch", "KiNSearch", connection, how, table, return_type, args, **options)
116
+
117
+
118
+ def search_image(
119
+ client_id: str,
120
+ client_secret: str,
121
+ query: str | Iterable[str],
122
+ start: int | Iterable[int] = 1,
123
+ display: int = 100,
124
+ sort: Literal["sim","date"] = "sim",
125
+ filter: Literal["all","large","medium","small"] = "all",
126
+ connection: DuckDBConnection | None = None,
127
+ how: Literal["sync","async","async_loop"] = "sync",
128
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
129
+ extract_options: dict | None = None,
130
+ transform_options: dict | None = None,
131
+ ) -> JsonObject:
132
+ args = (query, start, display, sort, filter)
133
+ table = get_table(transform_options, "table")
134
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
135
+ options = dict(extract_options=extract_options, transform_options=transform_options)
136
+ return run_with_duckdb(get_module(".search"), "ImageSearch", "ImageSearch", connection, how, table, return_type, args, **options)
137
+
138
+
139
+ def search_shop(
140
+ client_id: str,
141
+ client_secret: str,
142
+ query: str | Iterable[str],
143
+ start: int | Iterable[int] = 1,
144
+ display: int = 100,
145
+ sort: Literal["sim","date","asc","dsc"] = "sim",
146
+ connection: DuckDBConnection | None = None,
147
+ how: Literal["sync","async","async_loop"] = "sync",
148
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
149
+ extract_options: dict | None = None,
150
+ transform_options: dict | None = None,
151
+ ) -> JsonObject:
152
+ args = (query, start, display, sort)
153
+ table = get_table(transform_options, "table")
154
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
155
+ options = dict(extract_options=extract_options, transform_options=transform_options)
156
+ return run_with_duckdb(get_module(".search"), "ShoppingSearch", "ShoppingSearch", connection, how, table, return_type, args, **options)
157
+
158
+
159
+ def rank_shop(
160
+ client_id: str,
161
+ client_secret: str,
162
+ query: str | Iterable[str],
163
+ start: int | Iterable[int] = 1,
164
+ display: int = 100,
165
+ sort: Literal["sim","date","asc","dsc"] = "sim",
166
+ connection: DuckDBConnection | None = None,
167
+ how: Literal["sync","async","async_loop"] = "sync",
168
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
169
+ extract_options: dict | None = None,
170
+ transform_options: dict | None = None,
171
+ ) -> dict[str,JsonObject]:
172
+ args = (query, start, display, sort)
173
+ table = [get_table(transform_options, "rank_table"), get_table(transform_options, "product_table", "product")]
174
+ extract_options = dict(extract_options or dict(), variables=dict(client_id=client_id, client_secret=client_secret))
175
+ options = dict(extract_options=extract_options, transform_options=transform_options)
176
+ return run_with_duckdb(get_module(".search"), "ShoppingRank", "ShoppingRank", connection, how, table, return_type, args, **options)
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from linkmerce.common.api import run_with_duckdb
4
+ from linkmerce.common.api import get_table_from_options as get_table
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Iterable, Literal
10
+ from linkmerce.common.extract import JsonObject
11
+ from linkmerce.common.load import DuckDBConnection
12
+
13
+
14
+ def get_module(name: str) -> str:
15
+ return (".searchad.manage" + name) if name.startswith('.') else name
16
+
17
+
18
+ def exposure_diagnosis(
19
+ customer_id: str,
20
+ cookies: str,
21
+ keyword: str | Iterable[str],
22
+ domain: Literal["search","shopping"] = "search",
23
+ mobile: bool = True,
24
+ is_own: bool | None = None,
25
+ connection: DuckDBConnection | None = None,
26
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
27
+ extract_options: dict | None = None,
28
+ transform_options: dict | None = None,
29
+ ) -> JsonObject:
30
+ args = (keyword, domain, mobile, is_own)
31
+ table = get_table(transform_options, "table")
32
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies), variables=dict(customer_id=customer_id))
33
+ options = dict(extract_options=extract_options, transform_options=transform_options)
34
+ return run_with_duckdb(get_module(".exposure"), "ExposureDiagnosis", "ExposureDiagnosis", connection, "sync", table, return_type, args, **options)
35
+
36
+
37
+ def exposure_rank(
38
+ customer_id: str,
39
+ cookies: str,
40
+ keyword: str | Iterable[str],
41
+ domain: Literal["search","shopping"] = "search",
42
+ mobile: bool = True,
43
+ is_own: bool | None = None,
44
+ connection: DuckDBConnection | None = None,
45
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
46
+ extract_options: dict | None = None,
47
+ transform_options: dict | None = None,
48
+ ) -> JsonObject:
49
+ args = (keyword, domain, mobile, is_own)
50
+ table = [get_table(transform_options, "rank_table"), get_table(transform_options, "product_table", "product")]
51
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies), variables=dict(customer_id=customer_id))
52
+ options = dict(extract_options=extract_options, transform_options=transform_options)
53
+ return run_with_duckdb(get_module(".exposure"), "ExposureRank", "ExposureRank", connection, "sync", table, return_type, args, **options)
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from linkmerce.common.api import run_with_duckdb
4
+ from linkmerce.common.api import get_table_from_options as get_table
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from typing import Iterable, Literal
10
+ from linkmerce.common.extract import JsonObject
11
+ from linkmerce.common.load import DuckDBConnection
12
+ import datetime as dt
13
+
14
+
15
+ def get_module(name: str) -> str:
16
+ return (".smartstore.brand" + name) if name.startswith('.') else name
17
+
18
+
19
+ def brand_catalog(
20
+ cookies: str,
21
+ brand_ids: str | Iterable[str],
22
+ sort_type: Literal["popular","recent","price"] = "poular",
23
+ is_brand_catalog: bool | None = None,
24
+ page: int | list[int] | None = 0,
25
+ page_size: int = 100,
26
+ connection: DuckDBConnection | None = None,
27
+ how: Literal["sync","async","async_loop"] = "sync",
28
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
29
+ extract_options: dict | None = None,
30
+ transform_options: dict | None = None,
31
+ ) -> JsonObject:
32
+ args = (brand_ids, sort_type, is_brand_catalog, page, page_size)
33
+ table = get_table(transform_options, "table")
34
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
35
+ options = dict(extract_options=extract_options, transform_options=transform_options)
36
+ return run_with_duckdb(get_module(".catalog"), "BrandCatalog", "BrandCatalog", connection, how, table, return_type, args, **options)
37
+
38
+
39
+ def brand_product(
40
+ cookies: str,
41
+ brand_ids: str | Iterable[str],
42
+ mall_seq: int | str | Iterable[int | str] | None = None,
43
+ sort_type: Literal["popular","recent","price"] = "poular",
44
+ is_brand_catalog: bool | None = None,
45
+ page: int | list[int] | None = 0,
46
+ page_size: int = 100,
47
+ connection: DuckDBConnection | None = None,
48
+ how: Literal["sync","async","async_loop"] = "sync",
49
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
50
+ extract_options: dict | None = None,
51
+ transform_options: dict | None = None,
52
+ ) -> JsonObject:
53
+ args = (brand_ids, mall_seq, sort_type, is_brand_catalog, page, page_size)
54
+ table = get_table(transform_options, "table")
55
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
56
+ options = dict(extract_options=extract_options, transform_options=transform_options)
57
+ return run_with_duckdb(get_module(".catalog"), "BrandProduct", "BrandProduct", connection, how, table, return_type, args, **options)
58
+
59
+
60
+ def brand_price(
61
+ cookies: str,
62
+ brand_ids: str | Iterable[str],
63
+ mall_seq: int | str | Iterable[int | str],
64
+ sort_type: Literal["popular","recent","price"] = "recent",
65
+ is_brand_catalog: bool | None = None,
66
+ page: int | list[int] | None = 0,
67
+ page_size: int = 100,
68
+ connection: DuckDBConnection | None = None,
69
+ how: Literal["sync","async","async_loop"] = "sync",
70
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
71
+ extract_options: dict | None = None,
72
+ transform_options: dict | None = None,
73
+ ) -> JsonObject:
74
+ args = (brand_ids, mall_seq, sort_type, is_brand_catalog, page, page_size)
75
+ table = [get_table(transform_options, "price_table"), get_table(transform_options, "product_table", "product")]
76
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
77
+ options = dict(extract_options=extract_options, transform_options=transform_options)
78
+ return run_with_duckdb(get_module(".catalog"), "BrandPrice", "BrandPrice", connection, how, table, return_type, args, **options)
79
+
80
+
81
+ def store_sales(
82
+ cookies: str,
83
+ mall_seq: int | str | Iterable[int | str],
84
+ start_date: dt.date | str,
85
+ end_date: dt.date | str,
86
+ date_type: Literal["daily","weekly","monthly"] = "daily",
87
+ page: int | Iterable[int] = 1,
88
+ page_size: int = 1000,
89
+ connection: DuckDBConnection | None = None,
90
+ how: Literal["sync","async","async_loop"] = "sync",
91
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
92
+ extract_options: dict | None = None,
93
+ transform_options: dict | None = None,
94
+ ) -> JsonObject:
95
+ args = (mall_seq, start_date, end_date, date_type, page, page_size)
96
+ table = get_table(transform_options, "table")
97
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
98
+ options = dict(extract_options=extract_options, transform_options=transform_options)
99
+ return run_with_duckdb(get_module(".sales"), "StoreSales", "StoreSales", connection, how, table, return_type, args, **options)
100
+
101
+
102
+ def category_sales(
103
+ cookies: str,
104
+ mall_seq: int | str | Iterable[int | str],
105
+ start_date: dt.date | str,
106
+ end_date: dt.date | str,
107
+ date_type: Literal["daily","weekly","monthly"] = "daily",
108
+ page: int | Iterable[int] = 1,
109
+ page_size: int = 1000,
110
+ connection: DuckDBConnection | None = None,
111
+ how: Literal["sync","async","async_loop"] = "sync",
112
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
113
+ extract_options: dict | None = None,
114
+ transform_options: dict | None = None,
115
+ ) -> JsonObject:
116
+ args = (mall_seq, start_date, end_date, date_type, page, page_size)
117
+ table = get_table(transform_options, "table")
118
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
119
+ options = dict(extract_options=extract_options, transform_options=transform_options)
120
+ return run_with_duckdb(get_module(".sales"), "CategorySales", "CategorySales", connection, how, table, return_type, args, **options)
121
+
122
+
123
+ def product_sales(
124
+ cookies: str,
125
+ mall_seq: int | str | Iterable[int | str],
126
+ start_date: dt.date | str,
127
+ end_date: dt.date | str,
128
+ date_type: Literal["daily","weekly","monthly"] = "daily",
129
+ page: int | Iterable[int] = 1,
130
+ page_size: int = 1000,
131
+ connection: DuckDBConnection | None = None,
132
+ how: Literal["sync","async","async_loop"] = "sync",
133
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
134
+ extract_options: dict | None = None,
135
+ transform_options: dict | None = None,
136
+ ) -> JsonObject:
137
+ args = (mall_seq, start_date, end_date, date_type, page, page_size)
138
+ table = get_table(transform_options, "table")
139
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
140
+ options = dict(extract_options=extract_options, transform_options=transform_options)
141
+ return run_with_duckdb(get_module(".sales"), "ProductSales", "ProductSales", connection, how, table, return_type, args, **options)
142
+
143
+
144
+ def aggregated_sales(
145
+ cookies: str,
146
+ mall_seq: int | str | Iterable[int | str],
147
+ start_date: dt.date | str,
148
+ end_date: dt.date | str,
149
+ date_type: Literal["daily","weekly","monthly"] = "daily",
150
+ page: int | Iterable[int] = 1,
151
+ page_size: int = 1000,
152
+ connection: DuckDBConnection | None = None,
153
+ how: Literal["sync","async","async_loop"] = "sync",
154
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
155
+ extract_options: dict | None = None,
156
+ transform_options: dict | None = None,
157
+ ) -> JsonObject:
158
+ args = (mall_seq, start_date, end_date, date_type, page, page_size)
159
+ table = [get_table(transform_options, "sales_table"), get_table(transform_options, "product_table", "product")]
160
+ extract_options = dict(extract_options or dict(), headers=dict(cookies=cookies))
161
+ options = dict(extract_options=extract_options, transform_options=transform_options)
162
+ return run_with_duckdb(get_module(".sales"), "AggregatedSales", "AggregatedSales", connection, how, table, return_type, args, **options)
@@ -0,0 +1,186 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Sequence, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from typing import Any, Literal, Type
7
+ from linkmerce.common.extract import Extractor, JsonObject
8
+ from linkmerce.common.transform import Transformer, DBTransformer, DuckDBTransformer
9
+ from linkmerce.common.load import DuckDBConnection
10
+
11
+
12
+ ###################################################################
13
+ ############################## Import #############################
14
+ ###################################################################
15
+
16
+ def import_extractor(module: str, attr: str) -> Type[Extractor]:
17
+ from importlib import import_module
18
+ obj = import_module(_join(module, "extract"))
19
+ return getattr(obj, attr)
20
+
21
+
22
+ def import_transformer(module: str, attr: str) -> Type[Transformer]:
23
+ from importlib import import_module
24
+ obj = import_module(_join(module, "transform"))
25
+ return getattr(obj, attr)
26
+
27
+
28
+ def import_dbt(module: str | None = None, attr: str | None = None) -> Type[DBTransformer]:
29
+ if module and attr:
30
+ return import_transformer(module, attr)
31
+ else:
32
+ from importlib import import_module
33
+ module = import_module("linkmerce.common.transform")
34
+ return getattr(module, "DummyDBTransformer")
35
+
36
+
37
+ def _join(path: str, name: str) -> str:
38
+ if path.startswith('.'):
39
+ path = "linkmerce.core" + path
40
+ return path + '.' + name
41
+
42
+
43
+ ###################################################################
44
+ ############################### Run ###############################
45
+ ###################################################################
46
+
47
+ def run(
48
+ module: str,
49
+ extractor: str,
50
+ transformer: str | None = None,
51
+ how: Literal["sync","async","async_loop"] = "sync",
52
+ args: tuple = tuple(),
53
+ kwargs: dict = dict(),
54
+ extract_options: dict | None = None,
55
+ transform_options: dict | None = None,
56
+ ) -> Any:
57
+ extract_options = extract_options.copy() if isinstance(extract_options, dict) else dict()
58
+ if transformer and ("parser" not in extract_options):
59
+ transformer_ = import_transformer(module, transformer)(**(transform_options or dict()))
60
+ extract_options["parser"] = transformer_.transform
61
+ extractor_ = import_extractor(module, extractor)(**extract_options)
62
+ return _extract(extractor_, how, args, kwargs)
63
+
64
+
65
+ def run_with_connection(
66
+ module: str,
67
+ extractor: str,
68
+ transformer: str | None = None,
69
+ how: Literal["sync","async","async_loop"] = "sync",
70
+ args: tuple = tuple(),
71
+ kwargs: dict = dict(),
72
+ extract_options: dict | None = None,
73
+ transform_options: dict | None = None,
74
+ ) -> Any:
75
+ extract_options = extract_options.copy() if isinstance(extract_options, dict) else dict()
76
+ has_parser = ("parser" not in extract_options)
77
+ with import_dbt(module if has_parser else None, transformer)(**(transform_options or dict())) as transformer_:
78
+ if transformer and has_parser:
79
+ extract_options["parser"] = transformer_.transform
80
+ extractor_ = import_extractor(module, extractor)(**extract_options)
81
+ return _extract(extractor_, how, args, kwargs)
82
+
83
+
84
+ def _extract(
85
+ extractor: Extractor,
86
+ how: Literal["sync","async","async_loop"] = "sync",
87
+ args: tuple = tuple(),
88
+ kwargs: dict = dict(),
89
+ ) -> Any:
90
+ if how == "sync":
91
+ return extractor.extract(*args, **kwargs)
92
+ elif how == "async":
93
+ import asyncio
94
+ return asyncio.run(extractor.extract_async(*args, **kwargs))
95
+ elif how == "async_loop":
96
+ import asyncio, nest_asyncio
97
+ nest_asyncio.apply()
98
+ loop = asyncio.get_running_loop()
99
+ task = asyncio.create_task(extractor.extract_async(*args, **kwargs))
100
+ return loop.run_until_complete(task)
101
+ else:
102
+ raise ValueError("Invalid value for how to run. Supported values are: sync, async, async_loop.")
103
+
104
+
105
+ ###################################################################
106
+ ######################### Run with DuckDB #########################
107
+ ###################################################################
108
+
109
+ def run_with_duckdb(
110
+ module: str,
111
+ extractor: str,
112
+ transformer: str | None = None,
113
+ connection: DuckDBConnection | None = None,
114
+ how: Literal["sync","async","async_loop"] = "sync",
115
+ table: str | Sequence[str] = ":default:",
116
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
117
+ args: tuple = tuple(),
118
+ kwargs: dict = dict(),
119
+ extract_options: dict | None = None,
120
+ transform_options: dict | None = None,
121
+ ) -> Any | dict[str,Any]:
122
+ if connection is not None:
123
+ return run_with_duckdb_connection(
124
+ module, connection, extractor, transformer, how, table, return_type, args, kwargs, extract_options, transform_options)
125
+ extract_options = extract_options.copy() if isinstance(extract_options, dict) else dict()
126
+ has_parser = not ((return_type == "raw") or ("parser" in extract_options))
127
+ with import_dbt(module if has_parser else None, transformer)(**(transform_options or dict())) as transformer_:
128
+ if transformer and has_parser:
129
+ extract_options["parser"] = transformer_.transform
130
+ extractor_ = import_extractor(module, extractor)(**extract_options)
131
+ results = _extract(extractor_, how, args, kwargs)
132
+ return _fetch_all_from_table(transformer_, results, table, return_type)
133
+
134
+
135
+ def run_with_duckdb_connection(
136
+ module: str,
137
+ connection: DuckDBConnection,
138
+ extractor: str,
139
+ transformer: str | None = None,
140
+ how: Literal["sync","async","async_loop"] = "sync",
141
+ table: str | Sequence[str] = ":default:",
142
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
143
+ args: tuple = tuple(),
144
+ kwargs: dict = dict(),
145
+ extract_options: dict | None = None,
146
+ transform_options: dict | None = None,
147
+ ) -> Any | dict[str,Any]:
148
+ extract_options = extract_options.copy() if isinstance(extract_options, dict) else dict()
149
+ transform_options = dict(transform_options or dict(), db_info=dict(conn=connection))
150
+ has_parser = not ((return_type == "raw") or ("parser" in extract_options))
151
+ transformer_ = import_dbt(module, transformer)(**transform_options)
152
+ if transformer and has_parser:
153
+ extract_options["parser"] = transformer_.transform
154
+ extractor_ = import_extractor(module, extractor)(**extract_options)
155
+ results = _extract(extractor_, how, args, kwargs)
156
+ return _fetch_all_from_table(transformer_, results, table, return_type)
157
+
158
+
159
+ def get_table_from_options(transform_options: dict | None = None, key: str = "table", default: str = ":default:") -> str:
160
+ if isinstance(transform_options, dict):
161
+ create_options = transform_options.get("create_options")
162
+ if isinstance(create_options, dict):
163
+ return create_options.get(key, default)
164
+ return default
165
+
166
+
167
+ def _get_table(transformer: DuckDBTransformer, table: str = ":default:") -> str:
168
+ return transformer.default_table if table == ":default:" else table
169
+
170
+
171
+ def _fetch_all_from_table(
172
+ transformer: DuckDBTransformer,
173
+ results: JsonObject,
174
+ table: str | Sequence[str] = ":default:",
175
+ return_type: Literal["csv","json","parquet","raw","none"] = "json",
176
+ ) -> Any | dict[str,Any]:
177
+ if return_type == "none":
178
+ return
179
+ if (return_type == "raw") or (transformer.conn is None):
180
+ return results
181
+ elif isinstance(table, str):
182
+ return transformer.fetch_all(return_type, _get_table(transformer, table))
183
+ elif isinstance(table, Sequence):
184
+ return {_get_table(transformer, table_): transformer.fetch_all(return_type, table_) for table_ in table}
185
+ else:
186
+ raise TypeError("Invalid type for table. A string or sequence type is allowed.")
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class AuthenticationError(RuntimeError):
5
+ ...
6
+
7
+
8
+ class ParseError(ValueError):
9
+ ...
10
+
11
+
12
+ class RequestError(RuntimeError):
13
+ ...
14
+
15
+
16
+ class UnauthorizedError(RuntimeError):
17
+ ...