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.
- {kpubdata-0.2.1 → kpubdata-0.2.2}/ARCHITECTURE.md +14 -34
- {kpubdata-0.2.1 → kpubdata-0.2.2}/PKG-INFO +1 -1
- {kpubdata-0.2.1 → kpubdata-0.2.2}/PROVIDER_ADAPTER_CONTRACT.md +9 -1
- {kpubdata-0.2.1 → kpubdata-0.2.2}/pyproject.toml +1 -1
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/dataset.py +7 -2
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_dataset.py +37 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_to_pandas.py +29 -1
- {kpubdata-0.2.1 → kpubdata-0.2.2}/uv.lock +1 -1
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/ISSUE_TEMPLATE/new-provider-adapter.yml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/ci.yml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/integration.yml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.github/workflows/publish-pypi.yml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.gitignore +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/.sisyphus/remove-get-record-plan.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/AGENTS.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/API_SPEC.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/CANONICAL_MODEL.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/CHANGELOG.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/CONTRIBUTING.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/LICENSE +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/PACKAGING.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/PRD.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/README.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/ROADMAP.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/SUPPORTED_DATA.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/VALIDATION.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/adrs/0001-dialect-inspired-architecture.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/adrs/0002-standardize-ux-not-native-shape.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/architecture-diagrams.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/datago-api-reference.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/product-family-architecture.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/bok.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/datago.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/providers/kosis.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/quickstart.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/docs/tutorial-ai-agent-workflow.md +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/catalog.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/client.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/config.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/capability.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/models.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/protocol.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/core/representation.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/exceptions.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/_common.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/bok/catalogue.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/datago/catalogue.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/kosis/catalogue.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/providers/lofin/catalogue.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/py.typed +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/registry.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/decode.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/http.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/src/kpubdata/transport/retry.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/provider_adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_bok.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_datago.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_kosis.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/contract/test_lofin.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/error_auth.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/success_empty.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/bok/success_single_page.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_auth_30.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_invalid_request_10.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_rate_limit_22.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_service_unavailable_01.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/error_xml_auth_30.xml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_apt_trade.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_empty.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_multi_page_1.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_multi_page_2.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_single_item.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_single_page.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_string_numerics.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_xml.xml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/datago/success_xml_single_item.xml +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/error_auth.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/success_empty.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/kosis/success_single_page.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/error_auth.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/success_fiacrv.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/fixtures/lofin/success_single_page.json +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/conftest.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_bok_live.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_client_flow.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_datago_live.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_exception_propagation.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_kosis_live.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_lofin_live.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/integration/test_transport_http.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_dataset_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_exceptions.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_exceptions_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_models.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/core/test_models_fixtures.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/bok/test_bok_adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/conftest.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_adapter_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/datago/test_fixtures.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/providers/lofin/test_lofin_adapter.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalog.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalog_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_catalogue_validation.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_client_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_client_transport_requirements.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_config.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_config_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_registry.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/test_registry_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/__init__.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_decode.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_decode_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_http_coverage.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_http_coverage_extra.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_logging.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry_after.py +0 -0
- {kpubdata-0.2.1 → kpubdata-0.2.2}/tests/unit/transport/test_retry_coverage.py +0 -0
- {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
|
-
|
|
213
|
+
## 7. 페이지네이션 전략 (Pagination Strategy)
|
|
214
214
|
|
|
215
|
-
|
|
215
|
+
KPubData는 대량의 데이터를 효율적으로 가져오기 위해 두 가지 페이지네이션 전략을 사용합니다.
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
bok
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
231
|
+
## 8. 자주 묻는 질문 (FAQ)
|
|
251
232
|
|
|
252
|
-
## 7. 자주 묻는 질문 (FAQ)
|
|
253
233
|
|
|
254
234
|
**Q: 새 데이터셋을 추가하려면 어디를 수정하나요?**
|
|
255
235
|
A: 해당 기관의 어댑터(`providers/<provider>/adapter.py`)와 데이터 목록 파일(`catalogue.json`)을 수정하면 됩니다.
|
|
@@ -167,7 +167,15 @@ classDiagram
|
|
|
167
167
|
}
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
## 4.
|
|
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`
|
|
@@ -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
|
-
|
|
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"]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|