DNB unblocked + every metadata provider's language-name lookup hardened (#31)
Root cause: `cps/isoLanguages.py:get_language_names` accessed `locale.language` unguarded on the fallback branch. When called with a `None` locale (any code path outside a Flask request context — `auto_metadata.py:124` is one such caller) or a string locale that didn't exactly match a 28-key dict (e.g. `"en_US"`, `"en-GB"`, `"eng"`), it crashed with `AttributeError: 'NoneType' object has no attribute 'language'`.
DNB was the loud victim — every record's MARC21 041 language extraction tripped the bug — but every provider was at risk.
This release fixes the function once at the entry point. No provider changes.
| Locale input | Before | After |
|---|---|---|
| `None` | AttributeError | `None` |
| `'en'` | dict | dict |
| `'en_US'` | AttributeError | en dict (composite-locale fallback) |
| `'en-GB'` | AttributeError | en dict (hyphen-variant fallback) |
| `'eng'` | AttributeError | `None` (defensive) |
| Locale instance | dict | dict (regression-tested) |
Verified live on running instance
- DNB `search('Animal Farm george orwell', '', 'en')` → 2 records (was: 0 + AttributeError).
- All defensive paths return cleanly.
Upgrade
```bash
docker compose pull && docker compose up -d
```
No DB migration. No thumbnail refresh.
Tests
- 11 new pytest smoke cases pinning the contract.
- 10 new autopilot guards making sure the unguarded `.language` access never returns.
Full diff: v4.0.9...v4.0.10