linkmerce 0.2.1__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.1 → linkmerce-0.2.2}/PKG-INFO +2 -2
  2. {linkmerce-0.2.1 → linkmerce-0.2.2}/pyproject.toml +2 -2
  3. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/base.py +2 -36
  4. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/__init__.py +1 -1
  5. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/search.py +1 -1
  6. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/__init__.py +2 -2
  7. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/manage/exposure.py +1 -1
  8. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/catalog.py +1 -1
  9. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/sales.py +1 -1
  10. linkmerce-0.2.1/src/linkmerce/types.py → linkmerce-0.2.2/src/linkmerce/common.py +16 -0
  11. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/base.py +1 -1
  12. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/search.py +2 -2
  13. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/exposure.py +3 -3
  14. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/catalog.py +2 -2
  15. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/sales.py +3 -3
  16. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/duckdb.py +73 -45
  17. linkmerce-0.2.2/src/linkmerce/utils/pyarrow.py +1136 -0
  18. linkmerce-0.2.1/src/linkmerce/exceptions.py +0 -17
  19. linkmerce-0.2.1/src/linkmerce/extensions/bigquery.py +0 -195
  20. linkmerce-0.2.1/src/linkmerce/extensions/gspread.py +0 -262
  21. linkmerce-0.2.1/src/linkmerce/extensions/pandas.py +0 -114
  22. {linkmerce-0.2.1 → linkmerce-0.2.2}/README.md +0 -0
  23. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/__init__.py +0 -0
  24. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/auth/.DS_Store +0 -0
  25. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/auth/__init__.py +0 -0
  26. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/.DS_Store +0 -0
  27. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/__init__.py +0 -0
  28. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/naver/.DS_Store +0 -0
  29. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/naver/__init__.py +0 -0
  30. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/naver/api/.DS_Store +0 -0
  31. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/searchad/__init__.py +0 -0
  32. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/.DS_Store +0 -0
  33. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/__init__.py +0 -0
  34. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/smartstore/hcenter/__init__.py +0 -0
  35. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/collect/tasks.py +0 -0
  36. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/.DS_Store +0 -0
  37. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/__init__.py +0 -0
  38. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/naver/.DS_Store +0 -0
  39. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/naver/__init__.py +0 -0
  40. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/.DS_Store +0 -0
  41. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/naver/api/__init__.py +0 -0
  42. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/__init__.py +0 -0
  43. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/searchad/manage/__init__.py +0 -0
  44. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/.DS_Store +0 -0
  45. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/__init__.py +0 -0
  46. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/.DS_Store +0 -0
  47. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/parse/smartstore/hcenter/__init__.py +0 -0
  48. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/.DS_Store +0 -0
  49. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/__init__.py +0 -0
  50. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/cast.py +0 -0
  51. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/datetime.py +0 -0
  52. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/graphql.py +0 -0
  53. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/headers.py +0 -0
  54. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/jinja.py +0 -0
  55. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/map.py +0 -0
  56. {linkmerce-0.2.1 → linkmerce-0.2.2}/src/linkmerce/utils/tqdm.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: linkmerce
3
- Version: 0.2.1
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
10
  Requires-Dist: pytz>=2025.2
11
11
  Requires-Dist: requests>=2.32.4
12
12
  Requires-Dist: tqdm>=4.67.1
@@ -1,15 +1,15 @@
1
1
  [project]
2
2
  name = "linkmerce"
3
- version = "0.2.1"
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
13
  "pytz>=2025.2",
14
14
  "requests>=2.32.4",
15
15
  "tqdm>=4.67.1",
@@ -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):
@@ -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):
@@ -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
 
@@ -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
 
@@ -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
@@ -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
 
@@ -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
@@ -6,8 +6,9 @@ import functools
6
6
  from typing import TYPE_CHECKING
7
7
 
8
8
  if TYPE_CHECKING:
9
- from typing import Any, Generator, Literal, Sequence
9
+ from typing import Any, Literal, Sequence
10
10
  from duckdb import DuckDBPyConnection
11
+ from linkmerce.utils.pyarrow import Table
11
12
 
12
13
  DEFAULT_TEMP_TABLE = "temp_table"
13
14
 
@@ -79,6 +80,17 @@ def select_to_json(
79
80
  return [dict(zip(columns, row)) for row in relation.fetchall()]
80
81
 
81
82
 
83
+ def select_to_arrow(
84
+ query: str,
85
+ table: tuple[str,Table],
86
+ params: dict | None = None,
87
+ conn: DuckDBPyConnection | None = None,
88
+ ) -> Table:
89
+ with duckdb.connect() as conn:
90
+ conn.register(*table)
91
+ return conn.execute(query, parameters=params).arrow()
92
+
93
+
82
94
  ###################################################################
83
95
  ############################# Datetime ############################
84
96
  ###################################################################
@@ -200,52 +212,68 @@ def combine_first(
200
212
  ########################### Partition By ##########################
201
213
  ###################################################################
202
214
 
203
- def partition_by(
204
- data: list[dict],
205
- field: str,
206
- type: str | None = None,
207
- condition: str | None = None,
208
- sort: bool = True,
209
- ) -> Generator[list[dict], None, None]:
210
- conn = duckdb.connect()
211
- try:
212
- create_table(conn, DEFAULT_TEMP_TABLE, data, option="ignore", temp=True)
213
- if field not in get_columns(conn, DEFAULT_TEMP_TABLE):
214
- field = _add_partition(conn, DEFAULT_TEMP_TABLE, field, type)
215
- exclude = "EXCLUDE (_PARTITIONFIELD)" if field == "_PARTITIONFIELD" else str()
216
- for partition in _select_partition(conn, DEFAULT_TEMP_TABLE, field, condition, sort):
217
- yield select_to_json(
218
- f"SELECT * {exclude} FROM temp_table WHERE {field} = {_quote(partition)};", conn=conn)
219
- finally:
220
- conn.close()
221
-
222
-
223
- def _add_partition(
224
- conn: DuckDBPyConnection,
225
- table: str,
226
- expr: str,
227
- type: str | None = None,
228
- ) -> str:
229
- field = "_PARTITIONFIELD"
230
- if not type:
231
- type = conn.execute(f"SELECT {expr} FROM {table} LIMIT 1").description[0][TYPE]
232
- conn.execute(f"ALTER TABLE {table} ADD COLUMN {field} {type};")
233
- conn.execute(f"UPDATE {table} SET {field} = {expr};")
234
- return field
215
+ class Partition:
216
+ def __init__(
217
+ self,
218
+ data: list[dict],
219
+ field: str,
220
+ type: str | None = None,
221
+ condition: str | None = None,
222
+ sort: bool = True,
223
+ temp_table: str = DEFAULT_TEMP_TABLE,
224
+ ):
225
+ self.conn = duckdb.connect()
226
+ self.table = temp_table
227
+ try:
228
+ self.set_data(data)
229
+ self.set_field(field, type)
230
+ self.set_partitions(condition, sort)
231
+ except:
232
+ ...
233
+
234
+ def set_data(self, data: list[dict]):
235
+ create_table(self.conn, self.table, data, option="ignore", temp=True)
236
+
237
+ def set_field(self, field: str, type: str | None = None):
238
+ if field not in get_columns(self.conn, self.table):
239
+ field = "_PARTITIONFIELD"
240
+ if not type:
241
+ type = self.conn.execute(f"SELECT {field} FROM {self.table} LIMIT 1").description[0][TYPE]
242
+ self.conn.execute(f"ALTER TABLE {self.table} ADD COLUMN {field} {type};")
243
+ self.conn.execute(f"UPDATE {self.table} SET {field} = {field};")
244
+ self.field = field
245
+
246
+ def set_partitions(self, condition: str | None = None, sort: bool = True):
247
+ query = f"SELECT DISTINCT {self.field} FROM {self.table} {_where(condition, self.field)};"
248
+ if sort:
249
+ self.partitions = sorted(map(lambda x: x[0], self.conn.execute(query).fetchall()))
250
+ else:
251
+ self.partitions = [row[0] for row in self.conn.execute(query).fetchall()]
252
+
253
+ def __iter__(self) -> Partition:
254
+ self.index = 0
255
+ return self
256
+
257
+ def __next__(self) -> list[dict]:
258
+ if self.index < len(self):
259
+ exclude = "EXCLUDE (_PARTITIONFIELD)" if self.field == "_PARTITIONFIELD" else str()
260
+ query = f"SELECT * {exclude} FROM temp_table WHERE {self.field} = {_quote(self.partitions[self.index])};"
261
+ self.index += 1
262
+ return select_to_json(query, conn=self.conn)
263
+ else:
264
+ raise StopIteration
235
265
 
266
+ def __len__(self) -> int:
267
+ return len(self.partitions)
236
268
 
237
- def _select_partition(
238
- conn: DuckDBPyConnection,
239
- table: str,
240
- field: str,
241
- condition: str | None = None,
242
- sort: bool = True
243
- ) -> list[Any]:
244
- query = f"SELECT DISTINCT {field} FROM {table} {_where(condition, field)};"
245
- if sort:
246
- return sorted(map(lambda x: x[0], conn.execute(query).fetchall()))
247
- else:
248
- return [row[0] for row in conn.execute(query).fetchall()]
269
+ def __exit__(self):
270
+ self.close()
271
+
272
+ def close(self):
273
+ try:
274
+ self.conn.close()
275
+ except:
276
+ pass
249
277
 
250
278
 
251
279
  def _where(condition: str | None = None, field: str | None = None, **kwargs) -> str: