linkmerce 0.1.0__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.1.0/PKG-INFO +17 -0
- linkmerce-0.1.0/README.md +2 -0
- linkmerce-0.1.0/auth/.DS_Store +0 -0
- linkmerce-0.1.0/auth/__init__.py +0 -0
- linkmerce-0.1.0/collect/.DS_Store +0 -0
- linkmerce-0.1.0/collect/__init__.py +3 -0
- linkmerce-0.1.0/collect/base.py +317 -0
- linkmerce-0.1.0/object/__init__.py +0 -0
- linkmerce-0.1.0/pyproject.toml +24 -0
- linkmerce-0.1.0/utils/__init__.py +0 -0
- linkmerce-0.1.0/utils/headers.py +113 -0
- linkmerce-0.1.0/utils/pandas.py +115 -0
linkmerce-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: linkmerce
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: E-commerce API integration management
|
|
5
|
+
Requires-Dist: aiohttp>=3.12.14
|
|
6
|
+
Requires-Dist: asyncio>=3.4.3
|
|
7
|
+
Requires-Dist: bs4>=0.0.2
|
|
8
|
+
Requires-Dist: lxml>=6.0.0
|
|
9
|
+
Requires-Dist: openpyxl>=3.1.5
|
|
10
|
+
Requires-Dist: pandas>=2.3.1
|
|
11
|
+
Requires-Dist: requests>=2.32.4
|
|
12
|
+
Requires-Dist: xlrd>=2.0.2
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# LinkMerce
|
|
17
|
+
- E-commerce API integration management
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABCMeta, abstractmethod
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Union, TYPE_CHECKING
|
|
6
|
+
JsonObject = Union[Dict, List]
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any, IO, Literal, Sequence, Tuple
|
|
10
|
+
from requests import Session, Response
|
|
11
|
+
from requests.cookies import RequestsCookieJar
|
|
12
|
+
from aiohttp.client import ClientSession, ClientResponse
|
|
13
|
+
from aiohttp.typedefs import LooseCookies
|
|
14
|
+
from bs4 import BeautifulSoup
|
|
15
|
+
from pandas import DataFrame
|
|
16
|
+
JsonSerialize = Union[Dict, List, bytes, IO]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
GET = "GET"
|
|
20
|
+
POST = "POST"
|
|
21
|
+
OPTIONS = "OPTIONS"
|
|
22
|
+
HEAD = "HEAD"
|
|
23
|
+
PUT = "PUT"
|
|
24
|
+
PATCH = "PATCH"
|
|
25
|
+
DELETE = "DELETE"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseSessionClient(metaclass=ABCMeta):
|
|
29
|
+
method = GET
|
|
30
|
+
url = str()
|
|
31
|
+
|
|
32
|
+
def __init__(self, session: Session | ClientSession | None = None, **kwargs):
|
|
33
|
+
self.init_session(session)
|
|
34
|
+
self.init_constant(**kwargs)
|
|
35
|
+
|
|
36
|
+
def init_session(self, session: Session | ClientSession | None = None):
|
|
37
|
+
self.session = session
|
|
38
|
+
|
|
39
|
+
def init_constant(self, **kwargs):
|
|
40
|
+
self.set_request_headers(**kwargs)
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def request(self, **kwargs):
|
|
44
|
+
raise NotImplementedError("The request method must be implemented.")
|
|
45
|
+
|
|
46
|
+
def set_request_headers(self,
|
|
47
|
+
authority: str = str(),
|
|
48
|
+
accept: str = "*/*",
|
|
49
|
+
encoding: str = "gzip, deflate, br",
|
|
50
|
+
language: Literal["ko","en"] | str = "ko",
|
|
51
|
+
connection: str = "keep-alive",
|
|
52
|
+
contents: Literal["form", "javascript", "json", "text", "multipart"] | str | Dict = str(),
|
|
53
|
+
cookies: str = str(),
|
|
54
|
+
host: str = str(),
|
|
55
|
+
origin: str = str(),
|
|
56
|
+
priority: str = "u=0, i",
|
|
57
|
+
referer: str = str(),
|
|
58
|
+
client: str = str(),
|
|
59
|
+
mobile: bool = False,
|
|
60
|
+
platform: str = str(),
|
|
61
|
+
metadata: Literal["cors", "navigate"] | Dict[str,str] = "navigate",
|
|
62
|
+
https: bool = False,
|
|
63
|
+
user_agent: str = str(),
|
|
64
|
+
ajax: bool = False,
|
|
65
|
+
**kwargs):
|
|
66
|
+
from utils.headers import make_headers
|
|
67
|
+
kwargs = {key: value for key, value in locals().items() if key not in ("self","kwargs","make_headers")}
|
|
68
|
+
self.headers = make_headers(**kwargs)
|
|
69
|
+
|
|
70
|
+
def get_request_headers(self, **kwargs) -> Dict[str,str]:
|
|
71
|
+
return dict(self.headers, **kwargs) if kwargs else self.headers
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class RequestsSessionClient(BaseSessionClient):
|
|
75
|
+
def request(self,
|
|
76
|
+
method: str,
|
|
77
|
+
url: str,
|
|
78
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
79
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
80
|
+
json: JsonSerialize | None = None,
|
|
81
|
+
headers: Dict[str,str] = None,
|
|
82
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
83
|
+
**kwargs) -> Response:
|
|
84
|
+
return self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
85
|
+
|
|
86
|
+
def request_status(self,
|
|
87
|
+
method: str,
|
|
88
|
+
url: str,
|
|
89
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
90
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
91
|
+
json: JsonSerialize | None = None,
|
|
92
|
+
headers: Dict[str,str] = None,
|
|
93
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
94
|
+
**kwargs) -> int:
|
|
95
|
+
with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
96
|
+
return response.status_code
|
|
97
|
+
|
|
98
|
+
def request_content(self,
|
|
99
|
+
method: str,
|
|
100
|
+
url: str,
|
|
101
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
102
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
103
|
+
json: JsonSerialize | None = None,
|
|
104
|
+
headers: Dict[str,str] = None,
|
|
105
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
106
|
+
**kwargs) -> bytes:
|
|
107
|
+
with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
108
|
+
return response.content
|
|
109
|
+
|
|
110
|
+
def request_text(self,
|
|
111
|
+
method: str,
|
|
112
|
+
url: str,
|
|
113
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
114
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
115
|
+
json: JsonSerialize | None = None,
|
|
116
|
+
headers: Dict[str,str] = None,
|
|
117
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
118
|
+
**kwargs) -> str:
|
|
119
|
+
with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
120
|
+
return response.text
|
|
121
|
+
|
|
122
|
+
def request_json(self,
|
|
123
|
+
method: str,
|
|
124
|
+
url: str,
|
|
125
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
126
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
127
|
+
json: JsonSerialize | None = None,
|
|
128
|
+
headers: Dict[str,str] = None,
|
|
129
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
130
|
+
**kwargs) -> JsonObject:
|
|
131
|
+
with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
132
|
+
return response.json()
|
|
133
|
+
|
|
134
|
+
def request_headers(self,
|
|
135
|
+
method: str,
|
|
136
|
+
url: str,
|
|
137
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
138
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
139
|
+
json: JsonSerialize | None = None,
|
|
140
|
+
headers: Dict[str,str] = None,
|
|
141
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
142
|
+
**kwargs) -> Dict[str,str]:
|
|
143
|
+
with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
144
|
+
return response.headers
|
|
145
|
+
|
|
146
|
+
def request_html(self,
|
|
147
|
+
method: str,
|
|
148
|
+
url: str,
|
|
149
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
150
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
151
|
+
json: JsonSerialize | None = None,
|
|
152
|
+
headers: Dict[str,str] = None,
|
|
153
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
154
|
+
features: str | Sequence[str] | None = "html.parser",
|
|
155
|
+
**kwargs) -> BeautifulSoup:
|
|
156
|
+
from bs4 import BeautifulSoup
|
|
157
|
+
response = self.request_text(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
158
|
+
return BeautifulSoup(response, features)
|
|
159
|
+
|
|
160
|
+
def request_table(self,
|
|
161
|
+
method: str,
|
|
162
|
+
url: str,
|
|
163
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
164
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
165
|
+
json: JsonSerialize | None = None,
|
|
166
|
+
headers: Dict[str,str] = None,
|
|
167
|
+
cookies: Dict | RequestsCookieJar = None,
|
|
168
|
+
content_type: Literal["excel", "csv", "html", "xml"] | Sequence = "xlsx",
|
|
169
|
+
table_options: Dict = dict(),
|
|
170
|
+
**kwargs) -> DataFrame:
|
|
171
|
+
from utils.pandas import read_table
|
|
172
|
+
response = self.request_content(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
173
|
+
return read_table(response, table_format=content_type, **table_options)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AiohttpSessionClient(BaseSessionClient):
|
|
177
|
+
def request(self, *args, **kwargs):
|
|
178
|
+
raise NotImplementedError("This feature does not support synchronous requests. Please use the request_async method instead.")
|
|
179
|
+
|
|
180
|
+
async def request_async(self,
|
|
181
|
+
method: str,
|
|
182
|
+
url: str,
|
|
183
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
184
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
185
|
+
json: JsonSerialize | None = None,
|
|
186
|
+
headers: Dict[str,str] = None,
|
|
187
|
+
cookies: Dict | LooseCookies = None,
|
|
188
|
+
**kwargs) -> ClientResponse:
|
|
189
|
+
return await self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
190
|
+
|
|
191
|
+
async def request_async_status(self,
|
|
192
|
+
method: str,
|
|
193
|
+
url: str,
|
|
194
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
195
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
196
|
+
json: JsonSerialize | None = None,
|
|
197
|
+
headers: Dict[str,str] = None,
|
|
198
|
+
cookies: Dict | LooseCookies = None,
|
|
199
|
+
**kwargs) -> int:
|
|
200
|
+
async with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
201
|
+
return response.status
|
|
202
|
+
|
|
203
|
+
async def request_async_content(self,
|
|
204
|
+
method: str,
|
|
205
|
+
url: str,
|
|
206
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
207
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
208
|
+
json: JsonSerialize | None = None,
|
|
209
|
+
headers: Dict[str,str] = None,
|
|
210
|
+
cookies: Dict | LooseCookies = None,
|
|
211
|
+
**kwargs) -> bytes:
|
|
212
|
+
async with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
213
|
+
return response.content
|
|
214
|
+
|
|
215
|
+
async def request_async_text(self,
|
|
216
|
+
method: str,
|
|
217
|
+
url: str,
|
|
218
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
219
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
220
|
+
json: JsonSerialize | None = None,
|
|
221
|
+
headers: Dict[str,str] = None,
|
|
222
|
+
cookies: Dict | LooseCookies = None,
|
|
223
|
+
**kwargs) -> str:
|
|
224
|
+
async with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
225
|
+
return await response.text()
|
|
226
|
+
|
|
227
|
+
async def request_async_json(self,
|
|
228
|
+
method: str,
|
|
229
|
+
url: str,
|
|
230
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
231
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
232
|
+
json: JsonSerialize | None = None,
|
|
233
|
+
headers: Dict[str,str] = None,
|
|
234
|
+
cookies: Dict | LooseCookies = None,
|
|
235
|
+
**kwargs) -> JsonObject:
|
|
236
|
+
async with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
237
|
+
return await response.json()
|
|
238
|
+
|
|
239
|
+
async def request_async_headers(self,
|
|
240
|
+
method: str,
|
|
241
|
+
url: str,
|
|
242
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
243
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
244
|
+
json: JsonSerialize | None = None,
|
|
245
|
+
headers: Dict[str,str] = None,
|
|
246
|
+
cookies: Dict | LooseCookies = None,
|
|
247
|
+
**kwargs) -> Dict[str,str]:
|
|
248
|
+
async with self.session.request(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs) as response:
|
|
249
|
+
return response.headers
|
|
250
|
+
|
|
251
|
+
async def request_async_html(self,
|
|
252
|
+
method: str,
|
|
253
|
+
url: str,
|
|
254
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
255
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
256
|
+
json: JsonSerialize | None = None,
|
|
257
|
+
headers: Dict[str,str] = None,
|
|
258
|
+
cookies: Dict | LooseCookies = None,
|
|
259
|
+
features: str | Sequence[str] | None = "html.parser",
|
|
260
|
+
**kwargs) -> BeautifulSoup:
|
|
261
|
+
from bs4 import BeautifulSoup
|
|
262
|
+
response = await self.request_async_text(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
263
|
+
return BeautifulSoup(response, features)
|
|
264
|
+
|
|
265
|
+
async def request_async_table(self,
|
|
266
|
+
method: str,
|
|
267
|
+
url: str,
|
|
268
|
+
params: Dict | List[Tuple] | bytes | None = None,
|
|
269
|
+
data: Dict | List[Tuple] | bytes | IO | None = None,
|
|
270
|
+
json: JsonSerialize | None = None,
|
|
271
|
+
headers: Dict[str,str] = None,
|
|
272
|
+
cookies: Dict | LooseCookies = None,
|
|
273
|
+
content_type: Literal["excel", "csv", "html", "xml"] | Sequence = "xlsx",
|
|
274
|
+
table_options: Dict = dict(),
|
|
275
|
+
**kwargs) -> DataFrame:
|
|
276
|
+
from utils.pandas import read_table
|
|
277
|
+
response = await self.request_async_content(method, url, params=params, data=data, json=json, headers=headers, cookies=cookies, **kwargs)
|
|
278
|
+
return read_table(response, table_format=content_type, **table_options)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class Collector(RequestsSessionClient, AiohttpSessionClient, metaclass=ABCMeta):
|
|
282
|
+
@abstractmethod
|
|
283
|
+
def collect(self, *args, **kwargs) -> Any:
|
|
284
|
+
raise NotImplementedError("This feature does not support synchronous requests. Please use the collect_async method instead.")
|
|
285
|
+
|
|
286
|
+
def with_session(func):
|
|
287
|
+
@functools.wraps(func)
|
|
288
|
+
def wrapper(self: Collector, *args, init_session=False, **context):
|
|
289
|
+
if init_session and (self.session is None):
|
|
290
|
+
import requests
|
|
291
|
+
with requests.Session() as session:
|
|
292
|
+
self.init_session(session)
|
|
293
|
+
return func(self, *args, **context)
|
|
294
|
+
else:
|
|
295
|
+
return func(self, *args, **context)
|
|
296
|
+
return wrapper
|
|
297
|
+
|
|
298
|
+
async def collect_async(self, *args, **kwargs):
|
|
299
|
+
raise NotImplementedError("This feature does not support asynchronous requests. Please use the collect method instead.")
|
|
300
|
+
|
|
301
|
+
def with_client_session(func):
|
|
302
|
+
@functools.wraps(func)
|
|
303
|
+
async def wrapper(self: Collector, *args, init_session=False, **context):
|
|
304
|
+
if init_session and (self.session is None):
|
|
305
|
+
import aiohttp
|
|
306
|
+
async with aiohttp.ClientSession() as session:
|
|
307
|
+
self.init_session(session)
|
|
308
|
+
return await func(self, *args, **context)
|
|
309
|
+
else:
|
|
310
|
+
return await func(self, *args, **context)
|
|
311
|
+
return wrapper
|
|
312
|
+
|
|
313
|
+
def get_request_message(self, *args, **kwargs) -> Dict:
|
|
314
|
+
raise NotImplementedError("The get_request_message is not implemented.")
|
|
315
|
+
|
|
316
|
+
def parse(self, response: Any, *args, **kwargs) -> Any:
|
|
317
|
+
raise NotImplementedError("The parse method is not implemented.")
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "linkmerce"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "E-commerce API integration management"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"aiohttp>=3.12.14",
|
|
9
|
+
"asyncio>=3.4.3",
|
|
10
|
+
"bs4>=0.0.2",
|
|
11
|
+
"lxml>=6.0.0",
|
|
12
|
+
"openpyxl>=3.1.5",
|
|
13
|
+
"pandas>=2.3.1",
|
|
14
|
+
"requests>=2.32.4",
|
|
15
|
+
"xlrd>=2.0.2",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.8.0,<0.9.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-name = ["auth", "collect", "object", "utils"]
|
|
24
|
+
module-root = ""
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CHROME_VERSION = 138
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_headers(
|
|
12
|
+
authority: str = str(),
|
|
13
|
+
accept: str = "*/*",
|
|
14
|
+
encoding: str = "gzip, deflate, br",
|
|
15
|
+
language: Literal["ko","en"] | str = "ko",
|
|
16
|
+
connection: str = "keep-alive",
|
|
17
|
+
contents: Literal["form", "javascript", "json", "text", "multipart"] | str | Dict = str(),
|
|
18
|
+
cookies: str = str(),
|
|
19
|
+
host: str = str(),
|
|
20
|
+
origin: str = str(),
|
|
21
|
+
priority: str = "u=0, i",
|
|
22
|
+
referer: str = str(),
|
|
23
|
+
client: str = str(),
|
|
24
|
+
mobile: bool = False,
|
|
25
|
+
platform: str = str(),
|
|
26
|
+
metadata: Literal["cors", "navigate"] | Dict[str,str] = "cors",
|
|
27
|
+
https: bool = False,
|
|
28
|
+
user_agent: str = str(),
|
|
29
|
+
ajax: bool = False,
|
|
30
|
+
version: int = CHROME_VERSION
|
|
31
|
+
) -> Dict[str,str]:
|
|
32
|
+
from urllib.parse import urlparse
|
|
33
|
+
return {
|
|
34
|
+
**({"authority": authority} if authority else dict()),
|
|
35
|
+
**({"accept": accept} if accept else dict()),
|
|
36
|
+
**({"accept-encoding": encoding} if encoding else dict()),
|
|
37
|
+
**({"accept-language": _get_default_language(language)} if language else dict()),
|
|
38
|
+
**({"connection": connection} if connection else dict()),
|
|
39
|
+
**({"content-type": _get_content_type(contents)} if contents else dict()),
|
|
40
|
+
**({"cookie": cookies} if cookies else dict()),
|
|
41
|
+
**({"host": urlparse(host).hostname} if host else dict()),
|
|
42
|
+
**({"origin": origin} if origin else dict()),
|
|
43
|
+
**({"priority": priority} if priority else dict()),
|
|
44
|
+
**({"referer": referer} if referer else dict()),
|
|
45
|
+
"sec-ch-ua": (client or _get_default_client(version)),
|
|
46
|
+
"sec-ch-ua-mobile": f"?{int(mobile)}",
|
|
47
|
+
"sec-ch-ua-platform": (platform or _get_current_platform()),
|
|
48
|
+
**_get_fetch_metadata(metadata),
|
|
49
|
+
**({"upgrade-insecure-requests": "1"} if https else dict()),
|
|
50
|
+
"user-agent": (user_agent or _get_user_agent(version)),
|
|
51
|
+
**({"X-Requested-With": "XMLHttpRequest"} if ajax else dict())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _get_default_language(value: Literal["ko","en"] | str = "ko") -> str:
|
|
56
|
+
if value == "ko":
|
|
57
|
+
return "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7"
|
|
58
|
+
elif value == "en":
|
|
59
|
+
return "en-US,en;q=0.9"
|
|
60
|
+
else:
|
|
61
|
+
return value
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_content_type(contents: Literal["form", "javascript", "json", "text", "multipart"] | str | Dict):
|
|
65
|
+
if isinstance(contents, str):
|
|
66
|
+
if contents == "form":
|
|
67
|
+
return "application/x-www-form-urlencoded"
|
|
68
|
+
elif contents == "javascript":
|
|
69
|
+
return "javascript"
|
|
70
|
+
elif contents == "json":
|
|
71
|
+
return "application/json"
|
|
72
|
+
elif contents == "text":
|
|
73
|
+
return "text/plain"
|
|
74
|
+
elif contents == "multipart":
|
|
75
|
+
return "multipart/form-data"
|
|
76
|
+
else:
|
|
77
|
+
return contents
|
|
78
|
+
elif isinstance(contents, Dict):
|
|
79
|
+
content_type = _get_content_type(contents["type"])
|
|
80
|
+
for key, value in contents.items(): # boundary, charset, ...
|
|
81
|
+
if key != "type":
|
|
82
|
+
content_type += f"; {key}={value}"
|
|
83
|
+
return content_type
|
|
84
|
+
else:
|
|
85
|
+
raise TypeError("Invalid type for contents. A string or dictionary type is allowed.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _get_default_client(version: int = CHROME_VERSION) -> str:
|
|
89
|
+
return f'"Not)A;Brand";v="8", "Chromium";v="{version}", "Google Chrome";v="{version}"'
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _get_current_platform() -> str:
|
|
93
|
+
import platform
|
|
94
|
+
os_name = platform.system()
|
|
95
|
+
return "macOS" if os_name == "Darwin" else os_name
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_fetch_metadata(metadata: Literal["cors", "navigate"] | Dict[str,str] = "navigate") -> Dict[str,str]:
|
|
99
|
+
if isinstance(metadata, str):
|
|
100
|
+
if metadata == "cors":
|
|
101
|
+
return {"sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "sec-fetch-user": "?1"}
|
|
102
|
+
elif metadata == "navigate":
|
|
103
|
+
return {"sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "none", "sec-fetch-user": "?1"}
|
|
104
|
+
else:
|
|
105
|
+
return dict()
|
|
106
|
+
elif isinstance(metadata, Dict):
|
|
107
|
+
return metadata
|
|
108
|
+
else:
|
|
109
|
+
return dict()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _get_user_agent(version: int = CHROME_VERSION) -> str:
|
|
113
|
+
return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{version}.0.0.0 Safari/537.36"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from typing import Iterable, Sequence, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any, Callable, Dict, Hashable, List, Literal, Union
|
|
8
|
+
from io import BytesIO, StringIO
|
|
9
|
+
from pandas import DataFrame
|
|
10
|
+
from pandas._typing import DtypeArg
|
|
11
|
+
IndexLabel = Union[Sequence[Hashable], Hashable]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
TABLE_FORMAT = {".xlsx", ".xls", ".xlsm", ".xlsb", ".csv", ".html", ".xml"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def select_table(data: DataFrame, cols: IndexLabel, dropna: bool=False, reorder: bool=True) -> DataFrame:
|
|
18
|
+
from_cols, to_cols = (data.columns, _to_sequence(cols)) if reorder else (_to_sequence(cols), data.columns)
|
|
19
|
+
if dropna:
|
|
20
|
+
to_cols = [col for col in to_cols if col in from_cols]
|
|
21
|
+
else:
|
|
22
|
+
for col in cols:
|
|
23
|
+
if col not in data:
|
|
24
|
+
data[col] = None
|
|
25
|
+
return data[to_cols]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
###################################################################
|
|
29
|
+
############################ Read Table ###########################
|
|
30
|
+
###################################################################
|
|
31
|
+
|
|
32
|
+
def read_table(
|
|
33
|
+
io: bytes | str,
|
|
34
|
+
table_format: Literal["excel", "csv", "html", "xml"] | Sequence = "xlsx",
|
|
35
|
+
sheet_name: str | int | List | None = 0,
|
|
36
|
+
header: int | Sequence[int] | None = 0,
|
|
37
|
+
dtype: DtypeArg | None = None,
|
|
38
|
+
engine: Literal["xlrd", "openpyxl", "odf", "pyxlsb", "calamine"] | None = None,
|
|
39
|
+
parse_dates: bool | Sequence[int] | Sequence[Sequence[str] | Sequence[int]] | Dict[str, Sequence[int] | list[str]] = None,
|
|
40
|
+
file_pattern: Dict | None = None,
|
|
41
|
+
**kwargs
|
|
42
|
+
) -> DataFrame:
|
|
43
|
+
import pandas as pd
|
|
44
|
+
|
|
45
|
+
def read_io(io: BytesIO | StringIO, format: Literal["excel", "csv", "html", "xml"]) -> DataFrame:
|
|
46
|
+
if format == "excel":
|
|
47
|
+
if engine == "xlrd":
|
|
48
|
+
from xlrd import open_workbook
|
|
49
|
+
with open_workbook(file_contents=io.getvalue(), logfile=open(os.devnull, 'w')) as wb:
|
|
50
|
+
return pd.read_excel(wb, engine="xlrd", **kwargs)
|
|
51
|
+
else:
|
|
52
|
+
return pd.read_excel(io, header=header, dtype=dtype, engine=engine, parse_dates=parse_dates, **kwargs)
|
|
53
|
+
elif format == "csv":
|
|
54
|
+
return pd.read_csv(io, header=header, dtype=dtype, parse_dates=parse_dates, **kwargs)
|
|
55
|
+
elif format == "html":
|
|
56
|
+
index = sheet_name if isinstance(sheet_name, int) else 0
|
|
57
|
+
return pd.read_html(io, header=header, parse_dates=parse_dates, **kwargs)[index]
|
|
58
|
+
elif format == "xml":
|
|
59
|
+
return pd.read_xml(io, parse_dates=parse_dates, **kwargs)
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("Invalid value for table_format. Supported formats are: excel, csv, html, xml.")
|
|
62
|
+
|
|
63
|
+
if isinstance(table_format, str):
|
|
64
|
+
return read_io(_read_bytes(io, file_pattern), table_format)
|
|
65
|
+
elif isinstance(table_format, Sequence):
|
|
66
|
+
return _try_sequence(read_io,
|
|
67
|
+
kwargs = dict(format=_to_sequence(table_format)),
|
|
68
|
+
persist = dict(io=_read_bytes(io, file_pattern)))
|
|
69
|
+
else:
|
|
70
|
+
raise TypeError("Invalid type for table_format. A string or sequence type is allowed.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_bytes(io: bytes | str, file_pattern: Dict | None=None) -> BytesIO | StringIO:
|
|
74
|
+
from io import BytesIO, StringIO
|
|
75
|
+
if isinstance(io, str):
|
|
76
|
+
if (os.path.splitext(io)[1] in TABLE_FORMAT):
|
|
77
|
+
with open(_get_file_path(io, **(file_pattern or dict())), 'rb') as file:
|
|
78
|
+
return BytesIO(file.read())
|
|
79
|
+
else:
|
|
80
|
+
return StringIO(io)
|
|
81
|
+
else:
|
|
82
|
+
return BytesIO(io)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _get_file_path(io: str, regex: bool=False, index: int=0, **kwargs) -> str:
|
|
86
|
+
if regex:
|
|
87
|
+
import re
|
|
88
|
+
root, pattern = os.path.split(io)
|
|
89
|
+
def search(file_name: str) -> bool:
|
|
90
|
+
return bool(re.search(pattern, file_name))
|
|
91
|
+
filtered = list(filter(search, os.listdir(root or '.')))
|
|
92
|
+
io = os.path.join(root, filtered[index]) if -len(filtered) <= index < len(filtered) else str()
|
|
93
|
+
if os.path.exists(io):
|
|
94
|
+
return io
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError("File path does not exist: {}".format(f"'{io}'" if isinstance(io, str) else "None"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
###################################################################
|
|
100
|
+
############################# General #############################
|
|
101
|
+
###################################################################
|
|
102
|
+
|
|
103
|
+
def _to_sequence(cols: IndexLabel) -> Iterable[Hashable]:
|
|
104
|
+
return [cols] if isinstance(cols, str) or not isinstance(cols, Iterable) else cols
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _try_sequence(func: Callable, kwargs: Dict[str,Sequence]=dict(), persist: Dict=dict()) -> Any:
|
|
108
|
+
from itertools import product
|
|
109
|
+
args = list(map(lambda values: dict(zip(kwargs.keys(), values)),list(product(*kwargs.values()))))
|
|
110
|
+
for __m in args[:-1]:
|
|
111
|
+
try:
|
|
112
|
+
return func(**__m, **persist)
|
|
113
|
+
except:
|
|
114
|
+
pass
|
|
115
|
+
return func(**args[-1], **persist)
|