quickbase-extract 0.4.2__tar.gz → 0.4.4__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 (28) hide show
  1. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/CHANGELOG.md +15 -0
  2. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/PKG-INFO +1 -1
  3. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/pyproject.toml +1 -1
  4. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/cache_manager.py +2 -0
  5. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/cache_orchestration.py +9 -3
  6. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/report_data.py +14 -10
  7. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/.editorconfig +0 -0
  8. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/.gitignore +0 -0
  9. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/.pre-commit-config.yaml +0 -0
  10. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/.python-version +0 -0
  11. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/LICENSE.txt +0 -0
  12. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/README.md +0 -0
  13. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/TODO.md +0 -0
  14. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/__init__.py +0 -0
  15. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/api_handlers.py +0 -0
  16. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/cache_sync.py +0 -0
  17. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/config.py +0 -0
  18. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/py.typed +0 -0
  19. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/report_metadata.py +0 -0
  20. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/src/quickbase_extract/utils.py +0 -0
  21. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/conftest.py +0 -0
  22. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_api_handlers.py +0 -0
  23. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_cache_manager.py +0 -0
  24. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_cache_orchestration.py +0 -0
  25. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_cache_sync.py +0 -0
  26. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_report_data.py +0 -0
  27. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_report_metadata.py +0 -0
  28. {quickbase_extract-0.4.2 → quickbase_extract-0.4.4}/tests/test_utils.py +0 -0
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.4] - 2026-05-13
9
+
10
+ ### Fixed
11
+
12
+ - Changed `ask_values` parameter type annotations from `dict` to `Mapping` in `_refresh_data_cache` and `ensure_cache_freshness` in `cache_orchestration` to match `report_data` changes and accept any mapping type
13
+ - Fixed `data_reasons` variable in `ensure_cache_freshness` being potentially unbound if accessed outside the `data_caching_enabled` block; initialised to `[]` alongside other data cache variables
14
+ - Added `None` guard in `CacheManager._sync_to_s3` to raise `RuntimeError` with a clear message if called when `s3_client` is not configured, resolving type checker error and preventing unclear `AttributeError` at runtime
15
+
16
+ ## [0.4.3] - 2026-05-13
17
+
18
+ ### Fixed
19
+
20
+ - Changed `ask_values` parameter type annotations from `dict` to `Mapping` in `get_data`, `get_data_parallel`, `_validate_ask_values`, `_normalize_ask_values`, and `_replace_ask_placeholders` to accept any mapping type and correctly reflect read-only intent; added `from collections.abc import Mapping` import
21
+ - Added `None` guard for `query_data` before subscripting in `get_data` to prevent `TypeError` when `handle_query` returns `None`
22
+
8
23
  ## [0.4.2] - 2026-05-13
9
24
 
10
25
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickbase-extract
3
- Version: 0.4.2
3
+ Version: 0.4.4
4
4
  Summary: Extract and cache Quickbase report data with built-in error handling and S3 support
5
5
  Project-URL: Homepage, https://github.com/tbrezler/quickbase-extract
6
6
  Project-URL: Repository, https://github.com/tbrezler/quickbase-extract.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickbase-extract"
7
- version = "0.4.2"
7
+ version = "0.4.4"
8
8
  description = "Extract and cache Quickbase report data with built-in error handling and S3 support"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -173,6 +173,8 @@ class CacheManager:
173
173
  Raises:
174
174
  Exception: If upload fails. This is critical - Lambda /tmp is ephemeral.
175
175
  """
176
+ if self.s3_client is None:
177
+ raise RuntimeError("_sync_to_s3 called but s3_client is not configured")
176
178
  try:
177
179
  relative_path = file_path.relative_to(self.cache_root)
178
180
  s3_key = f"{self.s3_prefix}/{relative_path}" if self.s3_prefix else str(relative_path)
@@ -6,6 +6,7 @@ metadata and data caches. Ensures caches are up-to-date before processing.
6
6
 
7
7
  import logging
8
8
  import os
9
+ from collections.abc import Mapping
9
10
 
10
11
  from quickbase_extract.cache_manager import DEFAULT_DATA_STALE_HOURS, DEFAULT_METADATA_STALE_HOURS, CacheManager
11
12
  from quickbase_extract.config import ReportConfig
@@ -123,7 +124,7 @@ def _refresh_data_cache(
123
124
  cache_manager: CacheManager,
124
125
  reports_to_refresh: list[ReportConfig],
125
126
  reasons: list[str],
126
- ask_values: dict[ReportConfig, dict[str, str | list[str]]] | None = None,
127
+ ask_values: Mapping[ReportConfig, Mapping[str, str | list[str]]] | None = None,
127
128
  ) -> None:
128
129
  """Refresh data cache for specified reports.
129
130
 
@@ -132,8 +133,12 @@ def _refresh_data_cache(
132
133
  cache_manager: CacheManager instance.
133
134
  reports_to_refresh: Reports to refresh data for.
134
135
  reasons: List of reasons for refresh (for logging).
135
- ask_values: Optional dict mapping ReportConfig -> ask_values dict.
136
+ ask_values: Optional mapping of ReportConfig -> ask_values mapping.
136
137
  Per-report "ask the user" filter values.
138
+ data cache. Example: {
139
+ ReportConfig("bq8x", "Accounts", "Python"): {"ask1": "abc"},
140
+ ReportConfig("bq9y", "Contacts", "Active"): {"ask1": "def"}
141
+ }
137
142
 
138
143
  Raises:
139
144
  CacheRefreshError: If data refresh fails.
@@ -164,7 +169,7 @@ def ensure_cache_freshness(
164
169
  cache_manager: CacheManager,
165
170
  report_configs_all: list[ReportConfig],
166
171
  report_configs_to_cache: list[ReportConfig] | None = None,
167
- ask_values: dict[ReportConfig, dict[str, str | list[str]]] | None = None,
172
+ ask_values: Mapping[ReportConfig, Mapping[str, str | list[str]]] | None = None,
168
173
  metadata_stale_hours: float | None = None,
169
174
  data_stale_hours: float | None = None,
170
175
  cache_all_data: bool = False,
@@ -277,6 +282,7 @@ def ensure_cache_freshness(
277
282
  # Check data cache state and determine refresh needs (only if data caching enabled)
278
283
  data_needs_refresh = False
279
284
  reports_to_refresh_data = []
285
+ data_reasons: list[str] = []
280
286
  data_age = None
281
287
 
282
288
  if data_caching_enabled:
@@ -3,6 +3,7 @@
3
3
  import json
4
4
  import logging
5
5
  import re
6
+ from collections.abc import Mapping
6
7
  from concurrent.futures import ThreadPoolExecutor, as_completed
7
8
 
8
9
  from quickbase_extract.api_handlers import handle_query
@@ -13,7 +14,7 @@ logger = logging.getLogger(__name__)
13
14
 
14
15
 
15
16
  def _validate_ask_values(
16
- ask_values: dict[str, str | list[str]],
17
+ ask_values: Mapping[str, str | list[str]],
17
18
  placeholders_in_filter: set[str],
18
19
  report_config: ReportConfig,
19
20
  ) -> None:
@@ -57,7 +58,7 @@ def _validate_ask_values(
57
58
  )
58
59
 
59
60
 
60
- def _normalize_ask_values(ask_values: dict[str, str | list[str]]) -> dict[str, list[str]]:
61
+ def _normalize_ask_values(ask_values: Mapping[str, str | list[str]]) -> dict[str, list[str]]:
61
62
  """Normalize all ask_values to lists for consistent processing.
62
63
 
63
64
  Converts single string values to single-element lists, leaving list values
@@ -81,7 +82,7 @@ def _normalize_ask_values(ask_values: dict[str, str | list[str]]) -> dict[str, l
81
82
 
82
83
  def _replace_ask_placeholders(
83
84
  report_filter: str,
84
- ask_values: dict[str, str | list[str]],
85
+ ask_values: Mapping[str, str | list[str]],
85
86
  report_config: ReportConfig,
86
87
  ) -> str:
87
88
  """Replace ask-the-user placeholders in a Quickbase filter with actual values.
@@ -215,7 +216,7 @@ def get_data(
215
216
  report_config: ReportConfig,
216
217
  report_metadata: dict[ReportConfig, dict],
217
218
  cache: bool = False,
218
- ask_values: dict[str, str | list[str]] | None = None,
219
+ ask_values: Mapping[str, str | list[str]] | None = None,
219
220
  ) -> list[dict]:
220
221
  """Query a Quickbase table for data using cached report metadata.
221
222
 
@@ -270,6 +271,8 @@ def get_data(
270
271
  where=report_filter,
271
272
  sort_by=info["sort_by"],
272
273
  )
274
+ if query_data is None:
275
+ raise ValueError(f"Query returned no response for {report_config}")
273
276
  data = query_data["data"]
274
277
 
275
278
  # Transform records
@@ -300,18 +303,19 @@ def get_data_parallel(
300
303
  report_metadata: dict,
301
304
  cache: bool = False,
302
305
  max_workers: int = 8,
303
- ask_values: dict[ReportConfig, dict[str, str | list[str]]] | None = None,
306
+ ask_values: Mapping[ReportConfig, Mapping[str, str | list[str]]] | None = None,
304
307
  ) -> dict[ReportConfig, list[dict]]:
305
308
  """Fetch multiple reports in parallel using cached report metadata.
306
309
 
307
310
  Executes data fetching for multiple reports concurrently to improve
308
- performance. Uses a fail-fast approach: if any report fetch fails,
309
- all remaining tasks are cancelled and the exception is raised immediately.
311
+ performance. Uses a fail-fast approach: if any report fetch fails, the exception is raised
312
+ immediately and pending (not yet started) tasks will not be executed. Already
313
+ running tasks will complete before the exception propagates.
310
314
 
311
315
  Args:
312
316
  client: Quickbase API client. Should be thread-safe for concurrent use.
313
317
  cache_manager: CacheManager instance for cache operations.
314
- report_config: List of ReportConfig instances to fetch.
318
+ report_configs: List of ReportConfig instances to fetch.
315
319
  report_metadata: Full metadata dict (from load_report_metadata_batch).
316
320
  Keyed by ReportConfig instances.
317
321
  cache: Whether to cache retrieved data. Defaults to False.
@@ -331,7 +335,7 @@ def get_data_parallel(
331
335
  KeyError: If any report_config not found in report_metadata.
332
336
  ValueError: If ask placeholders in any filter are missing values.
333
337
  Exception: First exception encountered during parallel execution.
334
- All pending tasks are cancelled when an error occurs.
338
+ Pending tasks are not started; already running tasks complete first.
335
339
 
336
340
  Example:
337
341
  >>> cache_manager = CacheManager(cache_root=Path("my_project/dev/cache"))
@@ -440,7 +444,7 @@ def load_data_batch(
440
444
 
441
445
  Args:
442
446
  cache_manager: CacheManager instance for cache operations.
443
- report_config: List of ReportConfig instances to load.
447
+ report_configs: List of ReportConfig instances to load.
444
448
  report_metadata: Dict mapping ReportConfig -> metadata dict
445
449
  (from load_report_metadata_batch).
446
450