Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 1.16.1

- Always install `tqdm>=4.64` as a required dependency. Progress displays remain
opt-in with `show_progress=True` and tqdm stays lazily imported. Keep the
`progress` extra as a compatibility alias for existing installation commands.

## 1.16.0

- Add `install_archive` and `inspect_archive` for safely installing a complete
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ Python 3.9 or newer is required.

```sh
python -m pip install datacache
# Optional progress bars and HTML-table conversion:
python -m pip install "datacache[progress,html]"
# Optional HTML-table conversion:
python -m pip install "datacache[html]"
```

Progress is opt-in with `show_progress=True`. Normal use does not import tqdm
or configure your application's logging.
tqdm is installed by default; progress displays are opt-in with
`show_progress=True`. Normal use does not import tqdm or configure your
application's logging. The old `datacache[progress]` install syntax remains
supported as a compatibility alias.

The existing pandas dependency range is unchanged; upgrading DataCache does
not introduce a pandas 1.5 requirement. CI covers pandas 1.4.4, 1.5.3, and
Expand All @@ -44,8 +46,8 @@ assert cache.fetch(url, filename="LICENSE") == path

Replace the URL and filename with your dataset. Existing files are reused until
you explicitly refresh them with `force=True`; DataCache does not check whether
the remote file has changed. Add `show_progress=True` to display a download bar
after installing `datacache[progress]`.
the remote file has changed. Add `show_progress=True` to display a download bar;
no extra installation is needed.

### Find, inspect, or clear your cache

Expand Down Expand Up @@ -131,7 +133,7 @@ path = fetch_file(
destination="references/v1/records.tsv",
decompress=True,
timeout=30,
show_progress=True, # Requires datacache[progress].
show_progress=True,
)
```

Expand Down
6 changes: 3 additions & 3 deletions datacache/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _stream_to_file(
``progress_callback(bytes_downloaded, total_bytes)`` after each chunk is
written, where `total_bytes` is taken from the server's Content-Length
header (or None when the server doesn't report a size). This lets callers
drive e.g. a tqdm progress bar without datacache depending on tqdm.
drive an application's own progress display instead of the built-in bar.

Returns the total number of bytes written.
"""
Expand Down Expand Up @@ -586,11 +586,11 @@ def fetch_file(
If provided, called as ``progress_callback(bytes_downloaded,
total_bytes)`` after each chunk is written, where `total_bytes` is the
server-reported size or None if unknown. Lets callers render a progress
bar (e.g. tqdm) without datacache taking on that dependency.
display in their application's own UI instead of the built-in bar.

show_progress : bool, optional
Show tqdm download, decompression, and hash-verification progress.
Requires datacache[progress]. Defaults to False; cache hits are quiet.
tqdm is installed with datacache. Defaults to False; cache hits are quiet.
Can be combined with progress_callback. Each retry starts a fresh bar.

destination : str or os.PathLike, optional
Expand Down
6 changes: 3 additions & 3 deletions datacache/progress.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Optional progress displays. Importing datacache never imports tqdm."""
"""Opt-in progress displays using the required, lazily imported tqdm dependency."""


class Progress:
Expand All @@ -17,8 +17,8 @@ def __enter__(self):
from tqdm.auto import tqdm
except ImportError as error:
raise ImportError(
"Progress displays require tqdm; install 'datacache[progress]' "
"or use show_progress=False") from error
"tqdm is a required datacache dependency; install 'tqdm>=4.64' "
"or reinstall datacache") from error
self.bar = tqdm(
desc=self.description, total=self.total, unit=self.unit,
unit_scale=self.unit == "B", unit_divisor=1024,
Expand Down
2 changes: 1 addition & 1 deletion datacache/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.16.0"
__version__ = "1.16.1"
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Public API reference

This reference covers every name exported in `datacache.__all__` and every
public `Cache` method in DataCache 1.16.0. Import these names from `datacache`.
public `Cache` method in DataCache 1.16.1. Import these names from `datacache`.
Signatures below show all defaults; arguments after `*` are keyword-only.
Method signatures omit `self` and are called on a `Cache` instance.

Expand Down Expand Up @@ -103,7 +103,7 @@ replacement leaves the previous file intact.
| `max_retries` | Non-negative integer number of additional attempts after transient HTTP failures. Default `2` allows three total attempts; `0` disables retries. File and FTP downloads are not retried. |
| `retry_backoff` | Initial retry delay in seconds, doubled for subsequent retries. Must be finite and non-negative. |
| `retry_max_delay` | Maximum retry delay in seconds, finite and non-negative. A server `Retry-After` exceeding this limit stops retries. |
| `show_progress` | Boolean enabling optional tqdm download, decompression, and hash bars. Requires `datacache[progress]` when a bar is needed. Cache hits are quiet. |
| `show_progress` | Boolean enabling tqdm download, decompression, and hash bars. tqdm is installed with datacache; no extra is needed. Displays are opt-in and cache hits are quiet. |
| `resume` | Boolean retaining private partials for raw HTTP transfers. Requires size and a POSIX local filesystem. Without an expected SHA-256, every accepted response must supply a strong ETag. Size-only cache hits check only byte count. See [resumable downloads](downloads.md#resumable-http-downloads). |
| `record_provenance` | Boolean; after publishing a download, also write a hidden `.<name>.datacache.json` record of the source URL (without user name, password, query string, or fragment; the path is kept as is, so avoid recording URLs with secrets in their path), the fetch time, the size, and the SHA-256 when `expected_sha256` verified it. [inspect_file](#inspect_file) reports these offline. The record has the file's permissions. Default `False`: a caller that downloads to a temporary name and then moves the file would leave the record behind. Cache hits never write one, any new download removes a previous record first, and a failure to write one never fails the download. |
| `allow_empty` | Boolean accepting an empty installed file; default `False`. A complete but empty response, such as a withdrawn upstream record, is otherwise never published: HTTP retries it as transient, then raises `FileValidationError`. An empty cached file is likewise an invalid hit. `expected_size=0` also allows an empty file. |
Expand Down
2 changes: 1 addition & 1 deletion docs/integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ DataCache caches by path or database version, not by tracking remote content
changes. Refreshing a URL does not invalidate arbitrary downstream artifacts.
Libraries own dataset versions and their dependency relationships.

Progress is optional; install the `progress` extra when enabling tqdm. HTML
tqdm is installed by default; enable displays with `show_progress=True`. HTML
table conversion requires the `html` extra. The library does not install
logging handlers or change the process umask. Download retry counts and waits
are bounded, while `timeout` limits connection/read inactivity per attempt,
Expand Down
9 changes: 5 additions & 4 deletions docs/progress.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

See the [API reference](api.md) for signatures, defaults, return values, and errors.

Install the optional dependency with `python -m pip install "datacache[progress]"`.
tqdm is a required dependency, installed by `python -m pip install datacache`.
The old `datacache[progress]` extra remains a supported compatibility alias.
Set `show_progress=True` on `fetch_file`, `Cache.fetch`, or the CSV and database
helpers. DataCache uses [tqdm.auto](https://tqdm.github.io/docs/shortcuts/), which
selects a terminal or notebook display. Terminal bars use stderr; stdout remains
Expand All @@ -25,9 +26,9 @@ can show a checksum pass over an already cached file.
## Application callbacks

The existing `progress_callback(completed, total)` API remains supported and
can be used with or without tqdm. `completed` is cumulative for the current
download attempt; `total` may be `None`. Retries restart from zero, so counts
may decrease. Callbacks cover transfer bytes only, not transformation or
can be used with or without built-in progress bars. `completed` is cumulative
for the current download attempt; `total` may be `None`. Retries restart from
zero, so counts may decrease. Callbacks cover transfer bytes only, not transformation or
database work. Empty downloads do not invoke the callback.

```python
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ readme = "README.md"
dynamic = ["version", "dependencies"]

[project.optional-dependencies]
progress = ["tqdm>=4.64"]
# Compatibility alias: tqdm is now installed with the base package.
progress = []
html = ["lxml>=4.9"]
test = ["pytest>=7", "pytest-cov", "ruff", "tqdm>=4.64", "lxml>=4.9"]
test = ["pytest>=7", "pytest-cov", "ruff", "lxml>=4.9"]

[tool.setuptools.dynamic]
version = {attr = "datacache.version.__version__"}
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ appdirs>=1.4.0
requests>=2.5.1
typechecks>=0.0.2
filelock>=3.13
tqdm>=4.64
2 changes: 1 addition & 1 deletion tests/test_import_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def test_importing_datacache_loads_no_heavy_dependencies():
loaded = subprocess.run(
[sys.executable, "-c",
"import sys, datacache; "
"print(sorted(m for m in ('pandas', 'numpy', 'requests', 'urllib3') if m in sys.modules))"],
"print(sorted(m for m in ('pandas', 'numpy', 'requests', 'urllib3', 'tqdm') if m in sys.modules))"],
capture_output=True, text=True, check=True).stdout.strip()
assert loaded == "[]"

Expand Down
19 changes: 19 additions & 0 deletions tests/test_packaging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Check the dependencies consumers receive from a normal installation."""

from importlib.metadata import metadata, requires

from packaging.requirements import Requirement


def test_tqdm_is_an_unconditional_runtime_dependency():
dependencies = [Requirement(value) for value in requires("datacache")]
tqdm_dependencies = [item for item in dependencies if item.name == "tqdm"]
assert len(tqdm_dependencies) == 1
dependency = tqdm_dependencies[0]
assert dependency.marker is None
assert "4.64.0" in dependency.specifier
assert "4.63.0" not in dependency.specifier


def test_progress_extra_remains_a_compatible_alias():
assert "progress" in metadata("datacache").get_all("Provides-Extra", [])
4 changes: 2 additions & 2 deletions tests/test_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def test_database_progress_reports_completed_batches(tmp_path, bars):
assert bars[0].closed


def test_tqdm_is_optional_and_missing_extra_has_actionable_error(tmp_path, monkeypatch):
def test_missing_required_tqdm_has_actionable_error(tmp_path, monkeypatch):
original_import = builtins.__import__

def no_tqdm(name, *args, **kwargs):
Expand All @@ -155,7 +155,7 @@ def no_tqdm(name, *args, **kwargs):
source = tmp_path / "source"
source.write_bytes(b"data")
fetch_file(source.as_uri(), destination=tmp_path / "quiet")
with pytest.raises(ImportError, match=r"datacache\[progress\]"):
with pytest.raises(ImportError, match=r"tqdm is a required datacache dependency"):
fetch_file(source.as_uri(), destination=tmp_path / "progress", show_progress=True)
assert not (tmp_path / "progress").exists()
assert not list(tmp_path.glob(".datacache-*"))
Expand Down
Loading