Skip to content

Commit 542b1b9

Browse files
jason-raitzawilfox
andauthored
AP-564_extracting-tind-client (#1)
* refactor - roll fetch.py into client.py - removed fetch.py - remove direct api access - defaults for client init including default tind api key & url environment fetching - added basic .flake8 style settings - updated readme - updated tests * ENV handling and documentation Co-authored-by: Anna Wilcox <AWilcox@Wilcox-Tech.com>
1 parent afdda2f commit 542b1b9

File tree

10 files changed

+694
-390
lines changed

10 files changed

+694
-390
lines changed

.flake8

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[flake8]
2+
max-line-length = 100
3+
# Equivalent to allow-init-docstring, which we set on pydoclint.
4+
extend-ignore = DOC301,F401

.github/workflows/ci.yml

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,33 @@
11
name: CI
22

3-
on:
4-
push:
5-
branches: ["main"]
6-
pull_request:
7-
branches: ["main"]
8-
3+
on: [push, pull_request]
94
jobs:
105
test:
116
runs-on: ubuntu-latest
127
strategy:
138
matrix:
14-
python-version: ["3.13"]
9+
python-version:
10+
- "3.13"
11+
- "3.14"
1512

1613
steps:
17-
- uses: actions/checkout@v4
14+
- uses: actions/checkout@v6
1815

19-
- name: Set up Python ${{ matrix.python-version }}
20-
uses: actions/setup-python@v5
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@v7
2118
with:
19+
version: "0.10.7"
2220
python-version: ${{ matrix.python-version }}
21+
enable-cache: true
2322

24-
- name: Install dependencies
25-
run: |
26-
python -m pip install --upgrade pip
27-
pip install ".[test,lint]"
23+
- name: Sync dependencies
24+
run: uv sync --locked --all-extras --dev
2825

2926
- name: Lint with pylint
30-
run: pylint tind_client/
27+
run: uv run pylint tind_client tests
3128

3229
- name: Type-check with mypy
33-
run: mypy tind_client/
30+
run: uv run mypy tind_client tests
3431

3532
- name: Run tests
36-
run: pytest
33+
run: uv run pytest

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- Initial release
12-
- Basic project structure
1311
- Implemented TINDClient to wrap API interactions
12+
- Created tests, linting, and ci configurations
1413

1514
### Changed
1615
- N/A

README.md

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# python-tind-client
22

3-
Python library for interacting with the [TIND ILS](https://tind.io) API.
3+
Python library for interacting with the [TIND DA](https://tind.io) API.
44

55
## Requirements
66

@@ -23,14 +23,19 @@ pip install "python-tind-client[debug]" # debugpy
2323

2424
## Configuration
2525

26-
Create a `TINDClient` with explicit configuration values:
26+
Create a `TINDClient` with optional configuration values:
2727

28-
- `api_key` (required): Your TIND API token
29-
- `api_url` (required): Base URL of the TIND instance (e.g. `https://tind.example.edu`)
30-
- `default_storage_dir` (optional): Default output directory for downloaded files
28+
- `api_key` (optional): Your TIND API token. Falls back to the `TIND_API_KEY` environment variable.
29+
- `api_url` (optional): Base URL of the TIND instance (e.g. `https://tind.example.edu`). Falls back to the `TIND_API_URL` environment variable.
30+
- `default_storage_dir` (optional): Default output directory for downloaded files. Defaults to `./tmp`.
3131

3232
## Usage
3333

34+
### TIND Credentials / Optional ENV config
35+
You will need to either pass your TIND API credentials as arguments when instantiating a `TINDClient` or set them as environment variables with the following names:
36+
- `TIND_API_KEY`
37+
- `TIND_API_URL`
38+
3439
### instantiate a client
3540
```python
3641
from tind_client import TINDClient
@@ -44,39 +49,35 @@ client = TINDClient(
4449

4550
### Fetch pyMARC metadata for a record
4651
```python
47-
record = client.fetch_metadata("12345")
52+
record = client.fetch_metadata("116262")
4853
print(record["245"]["a"]) # title
4954
```
5055

5156
### Fetch file metadata for a record
5257
```python
53-
metadata = client.fetch_file_metadata("12345")
54-
print(metadata[0]) # first file metadata dict
55-
print(metadata[0]["url"]) # file download URL
58+
metadata = client.fetch_file_metadata("116262")
59+
print(metadata[0]) # first file metadata dict
60+
print(metadata[0]["url"]) # file download URL
5661
```
5762

5863
### Download a file
5964
```python
6065
# use metadata from previous example
61-
path_to_download = client.fetch_file(metadata[0].url)
66+
path_to_download = client.fetch_file(metadata[0]["url"])
6267
```
6368

64-
## Functional fetch API
65-
66-
The functions in `tind_client.fetch` are available for direct use and now accept
67-
explicit credentials instead of a client object.
68-
69+
### Search for records
6970
```python
70-
from tind_client.fetch import fetch_metadata
71+
# return a list of record IDs matching a query
72+
ids = client.fetch_ids_search("collection:'Disabled Students Program Photos'")
7173

72-
record = fetch_metadata(
73-
"12345",
74-
api_key="your-token",
75-
api_url="https://tind.example.edu",
76-
)
77-
```
74+
# return PyMARC records matching a query
75+
records = client.fetch_search_metadata("collection:'Disabled Students Program Photos'")
7876

79-
For most use cases, prefer `TINDClient` methods as the primary interface.
77+
# return raw XML or PyMARC records from a paginated search
78+
xml_results = client.search("collection:'Disabled Students Program Photos'", result_format="xml")
79+
pymarc_results = client.search("collection:'Disabled Students Program Photos'", result_format="pymarc")
80+
```
8081

8182
## Running tests
8283

pyproject.toml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,19 @@ testpaths = ["tests"]
4444

4545
[tool.mypy]
4646
python_version = "3.13"
47-
strict = true
47+
warn_unused_configs = true
48+
warn_redundant_casts = true
49+
warn_return_any = true
50+
strict_equality = true
51+
check_untyped_defs = true
52+
disallow_subclassing_any = true
53+
# We can't enable disallow_untyped_calls because of PyMARC.
54+
disallow_untyped_calls = false
55+
disallow_incomplete_defs = true
56+
disallow_untyped_defs = true
4857

49-
[tool.pylint.main]
50-
py-version = "3.13"
58+
[tool.pydoclint]
59+
allow-init-docstring = true
60+
skip-checking-raises = true
61+
style = "sphinx"
5162

52-
[tool.pylint.messages_control]
53-
disable = ["C0114"] # missing-module-docstring — already covered by file-level docstrings

tests/test_fetch.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
"""
2-
Tests for tind_client.fetch.
2+
Tests for TINDClient methods (fetch operations).
33
"""
44

55
import json
66

77
import pytest
88
import requests_mock as req_mock # noqa: F401 — activates the requests_mock fixture
99

10-
from tind_client import TINDClient, fetch
10+
from tind_client import TINDClient
1111
from tind_client.errors import RecordNotFoundError, TINDError
1212

1313
BASE_URL = "https://tind.example.edu"
@@ -28,17 +28,15 @@ def test_fetch_metadata_success(
2828
requests_mock.get(
2929
f"{BASE_URL}/record/12345/", text=sample_marc_xml, status_code=200
3030
)
31-
record = fetch.fetch_metadata(
32-
"12345", api_key=client.api_key, api_url=client.api_url
33-
)
31+
record = client.fetch_metadata("12345")
3432
assert record["245"]["a"] == "Sample Title"
3533

3634

3735
def test_fetch_metadata_404(requests_mock: req_mock.Mocker, client: TINDClient) -> None:
3836
"""fetch_metadata raises RecordNotFoundError on HTTP 404."""
3937
requests_mock.get(f"{BASE_URL}/record/99999/", text="", status_code=404)
4038
with pytest.raises(RecordNotFoundError):
41-
fetch.fetch_metadata("99999", api_key=client.api_key, api_url=client.api_url)
39+
client.fetch_metadata("99999")
4240

4341

4442
def test_fetch_metadata_empty_body(
@@ -47,7 +45,7 @@ def test_fetch_metadata_empty_body(
4745
"""fetch_metadata raises RecordNotFoundError when the response body is empty."""
4846
requests_mock.get(f"{BASE_URL}/record/11111/", text=" ", status_code=200)
4947
with pytest.raises(RecordNotFoundError):
50-
fetch.fetch_metadata("11111", api_key=client.api_key, api_url=client.api_url)
48+
client.fetch_metadata("11111")
5149

5250

5351
# ---------------------------------------------------------------------------
@@ -58,7 +56,7 @@ def test_fetch_metadata_empty_body(
5856
def test_fetch_file_invalid_url(client: TINDClient) -> None:
5957
"""fetch_file raises ValueError for non-TIND download URLs."""
6058
with pytest.raises(ValueError):
61-
fetch.fetch_file("https://not-a-tind-url.com/file.pdf", api_key=client.api_key)
59+
client.fetch_file("https://not-a-tind-url.com/file.pdf")
6260

6361

6462
def test_fetch_file_success(
@@ -74,7 +72,7 @@ def test_fetch_file_success(
7472
status_code=200,
7573
headers={"Content-Disposition": 'attachment; filename="document.pdf"'},
7674
)
77-
path = fetch.fetch_file(url, api_key=client.api_key, output_dir=str(tmp_path))
75+
path = client.fetch_file(url, output_dir=str(tmp_path))
7876
assert path.endswith("document.pdf")
7977

8078

@@ -87,7 +85,7 @@ def test_fetch_file_not_found(
8785
url = f"{BASE_URL}/files/missing/download"
8886
requests_mock.get(url, status_code=404)
8987
with pytest.raises(RecordNotFoundError):
90-
fetch.fetch_file(url, api_key=client.api_key, output_dir=str(tmp_path))
88+
client.fetch_file(url, output_dir=str(tmp_path))
9189

9290

9391
# ---------------------------------------------------------------------------
@@ -105,9 +103,7 @@ def test_fetch_file_metadata_success(
105103
text=json.dumps(payload),
106104
status_code=200,
107105
)
108-
result = fetch.fetch_file_metadata(
109-
"12345", api_key=client.api_key, api_url=client.api_url
110-
)
106+
result = client.fetch_file_metadata("12345")
111107
assert result[0]["name"] == "file.pdf"
112108

113109

@@ -121,9 +117,7 @@ def test_fetch_file_metadata_error(
121117
status_code=404,
122118
)
123119
with pytest.raises(TINDError):
124-
fetch.fetch_file_metadata(
125-
"12345", api_key=client.api_key, api_url=client.api_url
126-
)
120+
client.fetch_file_metadata("12345")
127121

128122

129123
# ---------------------------------------------------------------------------
@@ -140,9 +134,7 @@ def test_fetch_ids_search_success(
140134
text=json.dumps({"hits": ["1", "2", "3"]}),
141135
status_code=200,
142136
)
143-
ids = fetch.fetch_ids_search(
144-
"title:python", api_key=client.api_key, api_url=client.api_url
145-
)
137+
ids = client.fetch_ids_search("title:python")
146138
assert ids == ["1", "2", "3"]
147139

148140

@@ -156,9 +148,7 @@ def test_fetch_ids_search_error(
156148
status_code=400,
157149
)
158150
with pytest.raises(TINDError):
159-
fetch.fetch_ids_search(
160-
"title:python", api_key=client.api_key, api_url=client.api_url
161-
)
151+
client.fetch_ids_search("title:python")
162152

163153

164154
# ---------------------------------------------------------------------------
@@ -169,12 +159,7 @@ def test_fetch_ids_search_error(
169159
def test_search_invalid_format(client: TINDClient) -> None:
170160
"""search raises ValueError for unsupported result_format values."""
171161
with pytest.raises(ValueError, match="Unexpected result format"):
172-
fetch.search(
173-
"title:test",
174-
api_key=client.api_key,
175-
api_url=client.api_url,
176-
result_format="csv",
177-
)
162+
client.search("title:test", result_format="csv")
178163

179164

180165
def test_search_returns_xml(
@@ -191,12 +176,7 @@ def test_search_returns_xml(
191176

192177
requests_mock.get(f"{BASE_URL}/search", text=wrapped, status_code=200)
193178

194-
results = fetch.search(
195-
"title:sample",
196-
api_key=client.api_key,
197-
api_url=client.api_url,
198-
result_format="xml",
199-
)
179+
results = client.search("title:sample", result_format="xml")
200180
assert isinstance(results, list)
201181
assert len(results) >= 1
202182
assert requests_mock.call_count == 1

tind_client/__init__.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,15 @@
11
"""
2-
python-tind-client — Python library for interacting with the TIND ILS API.
2+
python-tind-client — Python library for interacting with the TIND DA API.
33
"""
44

55
__copyright__ = "© 2026 The Regents of the University of California. MIT license."
66

77
from .client import TINDClient
8-
from .api import tind_download, tind_get
98
from .errors import AuthorizationError, RecordNotFoundError, TINDError
10-
from .fetch import (
11-
fetch_file,
12-
fetch_file_metadata,
13-
fetch_ids_search,
14-
fetch_marc_by_ids,
15-
fetch_metadata,
16-
fetch_search_metadata,
17-
search,
18-
)
199

2010
__all__ = [
2111
"TINDClient",
22-
"tind_get",
23-
"tind_download",
2412
"AuthorizationError",
2513
"RecordNotFoundError",
2614
"TINDError",
27-
"fetch_metadata",
28-
"fetch_file",
29-
"fetch_file_metadata",
30-
"fetch_ids_search",
31-
"fetch_marc_by_ids",
32-
"fetch_search_metadata",
33-
"search",
3415
]

0 commit comments

Comments
 (0)