kpubdata 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 (139) hide show
  1. {kpubdata-0.2.1 → kpubdata-0.2.2}/ARCHITECTURE.md +14 -34
  2. {kpubdata-0.2.1 → kpubdata-0.2.2}/PKG-INFO +1 -1
  3. {kpubdata-0.2.1 → kpubdata-0.2.2}/PROVIDER_ADAPTER_CONTRACT.md +9 -1
  4. {kpubdata-0.2.1 → kpubdata-0.2.2}/pyproject.toml +1 -1
  5. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/dataset.py +7 -2
  6. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_dataset.py +37 -0
  7. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_to_pandas.py +29 -1
  8. {kpubdata-0.2.1 → kpubdata-0.2.2}/uv.lock +1 -1
  9. {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/ISSUE_TEMPLATE/new-provider-adapter.yml +0 -0
  10. {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/ci.yml +0 -0
  11. {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/integration.yml +0 -0
  12. {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
  13. {kpubdata-0.2.1 → kpubdata-0.2.2}/.gitignore +0 -0
  14. {kpubdata-0.2.1 → kpubdata-0.2.2}/.sisyphus/remove-get-record-plan.md +0 -0
  15. {kpubdata-0.2.1 → kpubdata-0.2.2}/AGENTS.md +0 -0
  16. {kpubdata-0.2.1 → kpubdata-0.2.2}/API_SPEC.md +0 -0
  17. {kpubdata-0.2.1 → kpubdata-0.2.2}/CANONICAL_MODEL.md +0 -0
  18. {kpubdata-0.2.1 → kpubdata-0.2.2}/CHANGELOG.md +0 -0
  19. {kpubdata-0.2.1 → kpubdata-0.2.2}/CONTRIBUTING.md +0 -0
  20. {kpubdata-0.2.1 → kpubdata-0.2.2}/LICENSE +0 -0
  21. {kpubdata-0.2.1 → kpubdata-0.2.2}/PACKAGING.md +0 -0
  22. {kpubdata-0.2.1 → kpubdata-0.2.2}/PRD.md +0 -0
  23. {kpubdata-0.2.1 → kpubdata-0.2.2}/README.md +0 -0
  24. {kpubdata-0.2.1 → kpubdata-0.2.2}/ROADMAP.md +0 -0
  25. {kpubdata-0.2.1 → kpubdata-0.2.2}/SUPPORTED_DATA.md +0 -0
  26. {kpubdata-0.2.1 → kpubdata-0.2.2}/VALIDATION.md +0 -0
  27. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/adrs/0001-dialect-inspired-architecture.md +0 -0
  28. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/adrs/0002-standardize-ux-not-native-shape.md +0 -0
  29. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/architecture-diagrams.md +0 -0
  30. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/datago-api-reference.md +0 -0
  31. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/product-family-architecture.md +0 -0
  32. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/bok.md +0 -0
  33. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/datago.md +0 -0
  34. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/kosis.md +0 -0
  35. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/quickstart.md +0 -0
  36. {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/tutorial-ai-agent-workflow.md +0 -0
  37. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/__init__.py +0 -0
  38. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/catalog.py +0 -0
  39. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/client.py +0 -0
  40. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/config.py +0 -0
  41. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/__init__.py +0 -0
  42. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/capability.py +0 -0
  43. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/models.py +0 -0
  44. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/protocol.py +0 -0
  45. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/representation.py +0 -0
  46. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/exceptions.py +0 -0
  47. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/__init__.py +0 -0
  48. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/_common.py +0 -0
  49. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/__init__.py +0 -0
  50. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/adapter.py +0 -0
  51. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/catalogue.json +0 -0
  52. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/__init__.py +0 -0
  53. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/adapter.py +0 -0
  54. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/catalogue.json +0 -0
  55. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/__init__.py +0 -0
  56. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/adapter.py +0 -0
  57. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/catalogue.json +0 -0
  58. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/__init__.py +0 -0
  59. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/adapter.py +0 -0
  60. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/catalogue.json +0 -0
  61. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/py.typed +0 -0
  62. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/registry.py +0 -0
  63. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/__init__.py +0 -0
  64. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/decode.py +0 -0
  65. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/http.py +0 -0
  66. {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/retry.py +0 -0
  67. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/__init__.py +0 -0
  68. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/__init__.py +0 -0
  69. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/provider_adapter.py +0 -0
  70. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_bok.py +0 -0
  71. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_datago.py +0 -0
  72. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_kosis.py +0 -0
  73. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_lofin.py +0 -0
  74. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/error_auth.json +0 -0
  75. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/success_empty.json +0 -0
  76. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/success_single_page.json +0 -0
  77. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_auth_30.json +0 -0
  78. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_invalid_request_10.json +0 -0
  79. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_rate_limit_22.json +0 -0
  80. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_service_unavailable_01.json +0 -0
  81. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_xml_auth_30.xml +0 -0
  82. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_apt_trade.json +0 -0
  83. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_empty.json +0 -0
  84. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_multi_page_1.json +0 -0
  85. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_multi_page_2.json +0 -0
  86. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_single_item.json +0 -0
  87. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_single_page.json +0 -0
  88. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_string_numerics.json +0 -0
  89. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_xml.xml +0 -0
  90. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_xml_single_item.xml +0 -0
  91. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/error_auth.json +0 -0
  92. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/success_empty.json +0 -0
  93. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/success_single_page.json +0 -0
  94. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/error_auth.json +0 -0
  95. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/success_fiacrv.json +0 -0
  96. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/success_single_page.json +0 -0
  97. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/__init__.py +0 -0
  98. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/conftest.py +0 -0
  99. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_bok_live.py +0 -0
  100. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_client_flow.py +0 -0
  101. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_datago_live.py +0 -0
  102. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_exception_propagation.py +0 -0
  103. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_kosis_live.py +0 -0
  104. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_lofin_live.py +0 -0
  105. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_transport_http.py +0 -0
  106. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/__init__.py +0 -0
  107. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/__init__.py +0 -0
  108. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_dataset_coverage.py +0 -0
  109. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_exceptions.py +0 -0
  110. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_exceptions_coverage.py +0 -0
  111. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_models.py +0 -0
  112. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_models_fixtures.py +0 -0
  113. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/__init__.py +0 -0
  114. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/bok/test_bok_adapter.py +0 -0
  115. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/__init__.py +0 -0
  116. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/conftest.py +0 -0
  117. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_adapter.py +0 -0
  118. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_adapter_coverage.py +0 -0
  119. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_fixtures.py +0 -0
  120. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/lofin/test_lofin_adapter.py +0 -0
  121. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalog.py +0 -0
  122. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalog_coverage.py +0 -0
  123. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalogue_validation.py +0 -0
  124. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_client_coverage.py +0 -0
  125. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_client_transport_requirements.py +0 -0
  126. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_config.py +0 -0
  127. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_config_coverage.py +0 -0
  128. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_registry.py +0 -0
  129. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_registry_coverage.py +0 -0
  130. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/__init__.py +0 -0
  131. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_decode.py +0 -0
  132. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_decode_coverage.py +0 -0
  133. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_http_coverage.py +0 -0
  134. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_http_coverage_extra.py +0 -0
  135. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_logging.py +0 -0
  136. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry.py +0 -0
  137. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry_after.py +0 -0
  138. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry_coverage.py +0 -0
  139. {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_transport_requirements.py +0 -0
@@ -210,46 +210,26 @@ sequenceDiagram
210
210
 
211
211
  **핵심 메서드**: 모든 어댑터는 `_require_api_key()` → `self._config.require_provider_key("slug")` 패턴을 사용합니다.
212
212
 
213
- ### 6.4 기관별 주입 방식
213
+ ## 7. 페이지네이션 전략 (Pagination Strategy)
214
214
 
215
- 기관마다 키를 HTTP 요청에 넣는 위치와 파라미터 이름이 다릅니다.
215
+ KPubData는 대량의 데이터를 효율적으로 가져오기 위해 가지 페이지네이션 전략을 사용합니다.
216
216
 
217
- | Provider | 파라미터 이름 | 주입 위치 | 예시 | 특이사항 |
218
- |---|---|---|---|---|
219
- | `datago` | `serviceKey` | Query parameter | `?serviceKey=KEY` | dataset별 `service_key_param` 메타데이터로 파라미터명 오버라이드 가능 |
220
- | `bok` | _(URL 경로)_ | URL path segment | `/{KEY}/json/{operation}/...` | 쿼리 파라미터가 아닌 **URL 경로**에 직접 삽입 |
221
- | `kosis` | `apiKey` | Query parameter | `?apiKey=KEY` | 표준적인 쿼리 파라미터 방식 |
222
- | `lofin` | `Key` | Query parameter | `?Key=KEY` | 대문자 `K` 필수. `Type=json`도 대문자 `T` 필수 |
217
+ ### 7.1 전략 종류
218
+ - **오프셋 기반 (Offset-based)**: 페이지 번호를 이용해 데이터를 요청합니다. `next_page` 값이 반환됩니다.
219
+ - **커서 기반 (Cursor-based)**: 특정 지점을 가리키는 포인터(커서)를 이용해 다음 데이터를 요청합니다. `next_cursor` 값이 반환됩니다.
223
220
 
224
- ```text
225
- datago: GET https://apis.data.go.kr/.../getVilageFcst?serviceKey=KEY&...
226
- bok: GET https://ecos.bok.or.kr/api/KEY/json/StatisticSearch/...
227
- kosis: GET https://kosis.kr/openapi/Idx/indikatorList.do?apiKey=KEY&...
228
- lofin: GET https://www.lofin365.go.kr/lf/hub/AJGCF?Key=KEY&Type=json&...
229
- ```
230
-
231
- ### 6.5 새 어댑터 추가 시 인증 구현 가이드
232
-
233
- 새로운 데이터 기관을 추가할 때, 인증 관련 구현은 다음 3단계를 따릅니다.
234
-
235
- **1단계**: `_require_api_key()` 메서드를 정의합니다.
236
- ```python
237
- def _require_api_key(self) -> str:
238
- return self._config.require_provider_key("your_provider_slug")
239
- ```
221
+ ### 7.2 동작 원리 및 휴리스틱 (Heuristic)
222
+ 어댑터는 가용한 정보에 따라 다음과 같이 다음 페이지 존재 여부를 결정합니다.
223
+ 1. **정밀 계산**: `total_count` 정보를 알 수 있는 경우(예: bok, lofin), 현재까지 가져온 개수와 전체 개수를 비교해 `next_page`를 정확히 계산합니다.
224
+ 2. **Best-effort 휴리스틱**: 전체 개수 정보가 없는 경우(예: datago), `len(items) == page_size` 공식을 사용합니다.
225
+ - 현재 페이지의 아이템 개수가 요청한 페이지 크기와 같다면, 다음 페이지가 더 있을 것으로 가정합니다.
226
+ - 이 방식은 마지막 데이터가 딱 페이지 크기에 맞춰 끝날 경우, 실제로 데이터가 없는 다음 페이지를 한 번 더 호출하는 **추가 fetch(Extra empty fetch)**가 발생할 수 있습니다. 이는 문서화된 트레이드오프(trade-off)입니다.
240
227
 
241
- **2단계**: 요청 생성 시 키를 해당 기관의 방식에 맞게 주입합니다.
242
- ```python
243
- # Query parameter 방식 (가장 일반적)
244
- params = {"apiKey": self._require_api_key(), ...}
245
-
246
- # URL path 방식 (bok처럼 경로에 넣는 경우)
247
- url = f"{base_url}/{self._require_api_key()}/json/{operation}"
248
- ```
228
+ ### 7.3 list_all()의 처리
229
+ 사용자가 `list_all()` 메서드를 호출하면, KPubData는 어댑터가 반환하는 전략(`next_cursor` 우선, 없으면 `next_page`)을 자동으로 판단하여 모든 데이터를 순회합니다.
249
230
 
250
- **3단계**: `Client`의 `_BUILTIN_PROVIDERS` 튜플에 등록하면, `from_env()`가 자동으로 `KPUBDATA_{SLUG}_API_KEY` 환경변수를 스캔합니다.
231
+ ## 8. 자주 묻는 질문 (FAQ)
251
232
 
252
- ## 7. 자주 묻는 질문 (FAQ)
253
233
 
254
234
  **Q: 새 데이터셋을 추가하려면 어디를 수정하나요?**
255
235
  A: 해당 기관의 어댑터(`providers/<provider>/adapter.py`)와 데이터 목록 파일(`catalogue.json`)을 수정하면 됩니다.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kpubdata
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Dialect-inspired Python access framework for Korean public data
5
5
  Author: Yeongseon Choe
6
6
  License: MIT
@@ -167,7 +167,15 @@ classDiagram
167
167
  }
168
168
  ```
169
169
 
170
- ## 4. Capability rules
170
+ ## 4. 페이지네이션 반환 규약 (Pagination Return Contract)
171
+
172
+ 어댑터는 `query_records` 결과로 반환되는 `RecordBatch`에 다음 페이지 정보를 포함해야 합니다.
173
+
174
+ - **`next_page` 또는 `next_cursor` 반환**: 어댑터는 반드시 둘 중 하나를 반환하거나, 마지막 페이지인 경우 둘 다 `None`을 반환해야 합니다.
175
+ - **우선순위**: `list_all()`은 `next_cursor`가 존재할 경우 이를 우선적으로 사용하며, 없을 경우 `next_page`를 사용합니다.
176
+ - **권장 방식**: 가급적 `total_count` 기반의 정밀한 계산 방식을 선호합니다. 하지만 전체 개수 정보를 알 수 없는 경우 `len(items) == page_size` 휴리스틱을 사용하는 것도 허용됩니다.
177
+
178
+ ## 5. Capability rules
171
179
 
172
180
  - declare only what the adapter truly supports
173
181
  - if an operation is unavailable, raise `UnsupportedCapabilityError`
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "kpubdata"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Dialect-inspired Python access framework for Korean public data"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -135,8 +135,13 @@ class Dataset:
135
135
  page_kwargs = dict(kwargs)
136
136
  batch = self.list(**page_kwargs)
137
137
  yield batch
138
- while batch.next_page is not None:
139
- page_kwargs["page"] = batch.next_page
138
+ while batch.next_page is not None or batch.next_cursor is not None:
139
+ if batch.next_cursor is not None:
140
+ page_kwargs["cursor"] = batch.next_cursor
141
+ _ = page_kwargs.pop("page", None)
142
+ else:
143
+ page_kwargs["page"] = batch.next_page
144
+ _ = page_kwargs.pop("cursor", None)
140
145
  batch = self.list(**page_kwargs)
141
146
  yield batch
142
147
 
@@ -76,6 +76,12 @@ class TestDataset:
76
76
  with pytest.raises(UnsupportedCapabilityError, match="list"):
77
77
  _ = ds.list()
78
78
 
79
+ def test_list_all_unsupported(self) -> None:
80
+ adapter = MockAdapter()
81
+ ds = Dataset(ref=_ref(frozenset({Operation.RAW})), adapter=adapter)
82
+ with pytest.raises(UnsupportedCapabilityError, match="list"):
83
+ _ = list(ds.list_all())
84
+
79
85
  def test_call_raw(self) -> None:
80
86
  adapter = MockAdapter()
81
87
  ds = Dataset(ref=_ref(), adapter=adapter)
@@ -170,3 +176,34 @@ class TestDataset:
170
176
 
171
177
  assert len(batches) == 1
172
178
  assert batches[0].items == [{"page": 1}]
179
+
180
+ def test_list_all_cursor_pagination(self) -> None:
181
+ adapter = MockAdapter()
182
+ dataset_ref = _ref()
183
+ adapter.batches = [
184
+ RecordBatch(items=[{"cursor": "a"}], dataset=dataset_ref, next_cursor="abc"),
185
+ RecordBatch(items=[{"cursor": "b"}], dataset=dataset_ref, next_cursor="def"),
186
+ RecordBatch(items=[{"cursor": "c"}], dataset=dataset_ref, next_cursor=None),
187
+ ]
188
+ ds = Dataset(ref=dataset_ref, adapter=adapter)
189
+
190
+ batches = list(ds.list_all(region="서울"))
191
+
192
+ assert [batch.items[0]["cursor"] for batch in batches] == ["a", "b", "c"]
193
+ assert adapter.last_query is not None
194
+ assert adapter.last_query.cursor == "def"
195
+ assert adapter.last_query.page is None
196
+ assert adapter.last_query.filters == {"region": "서울"}
197
+
198
+ def test_list_all_cursor_single_page(self) -> None:
199
+ adapter = MockAdapter()
200
+ dataset_ref = _ref()
201
+ adapter.batches = [
202
+ RecordBatch(items=[{"cursor": "a"}], dataset=dataset_ref, next_cursor=None)
203
+ ]
204
+ ds = Dataset(ref=dataset_ref, adapter=adapter)
205
+
206
+ batches = list(ds.list_all())
207
+
208
+ assert len(batches) == 1
209
+ assert batches[0].items == [{"cursor": "a"}]
@@ -34,7 +34,7 @@ class _FakeDataFrame:
34
34
 
35
35
 
36
36
  class _FakePandasModule(ModuleType):
37
- DataFrame: type[_FakeDataFrame]
37
+ DataFrame: type[_FakeDataFrame] = _FakeDataFrame
38
38
 
39
39
 
40
40
  def _fake_pandas_module() -> _FakePandasModule:
@@ -80,3 +80,31 @@ def test_to_pandas_import_error() -> None:
80
80
  pytest.raises(ImportError, match=r"pandas is required for to_pandas\(\)"),
81
81
  ):
82
82
  _ = batch.to_pandas()
83
+
84
+
85
+ def test_to_pandas_real_pandas_if_available() -> None:
86
+ from typing import Protocol, cast
87
+
88
+ class _DataFrameLike(Protocol):
89
+ shape: tuple[int, int]
90
+ columns: list[str]
91
+
92
+ class _PandasModule(Protocol):
93
+ DataFrame: type[_DataFrameLike]
94
+
95
+ pd = cast(_PandasModule, pytest.importorskip("pandas"))
96
+
97
+ batch = RecordBatch(
98
+ items=[
99
+ {"name": "Alice", "age": 30},
100
+ {"name": "Bob", "age": 31},
101
+ {"name": "Carol", "age": 32},
102
+ ],
103
+ dataset=_dataset_ref(),
104
+ )
105
+
106
+ df = batch.to_pandas()
107
+
108
+ assert isinstance(df, pd.DataFrame)
109
+ assert df.shape == (3, 2)
110
+ assert list(df.columns) == ["name", "age"]
@@ -462,7 +462,7 @@ wheels = [
462
462
 
463
463
  [[package]]
464
464
  name = "kpubdata"
465
- version = "0.2.1"
465
+ version = "0.2.2"
466
466
  source = { editable = "." }
467
467
  dependencies = [
468
468
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes