python-fotoware-client 0.2.0__py3-none-any.whl
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.
- fotoware_client/__init__.py +62 -0
- fotoware_client/auth.py +302 -0
- fotoware_client/client.py +919 -0
- fotoware_client/constants.py +13 -0
- fotoware_client/credentials.py +59 -0
- fotoware_client/exceptions.py +110 -0
- fotoware_client/limits.py +133 -0
- fotoware_client/models.py +190 -0
- fotoware_client/py.typed +0 -0
- fotoware_client/retry.py +166 -0
- fotoware_client/settings.py +54 -0
- python_fotoware_client-0.2.0.dist-info/METADATA +160 -0
- python_fotoware_client-0.2.0.dist-info/RECORD +16 -0
- python_fotoware_client-0.2.0.dist-info/WHEEL +5 -0
- python_fotoware_client-0.2.0.dist-info/licenses/LICENSE +21 -0
- python_fotoware_client-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""What a FotoWare installation may set differently, and the client must follow.
|
|
2
|
+
|
|
3
|
+
The defaults are what FotoWeb 8.1.1223.0 does out of the box, measured on
|
|
4
|
+
2026-10-23 (see fotoware_client.limits). An installation that renamed its
|
|
5
|
+
original rendition, serves FotoWeb under another path, prepares renditions
|
|
6
|
+
slowly or translates queries differently passes its own ServerSettings.
|
|
7
|
+
|
|
8
|
+
What is FotoWare's API itself -- the media types, the query syntax, paging
|
|
9
|
+
through ``paging.next`` -- is not here: it is the same everywhere.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
from fotoware_client.limits import QUERY_SIZE_BUDGET, TERM_OVERHEAD
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class ServerSettings:
|
|
21
|
+
"""A FotoWare installation's own settings, as the client must know them.
|
|
22
|
+
|
|
23
|
+
Attributes:
|
|
24
|
+
oauth_token_path: Where the installation issues OAuth tokens.
|
|
25
|
+
api_descriptor_path: The API descriptor, which links the archives.
|
|
26
|
+
original_rendition_name: The display name of the original-file
|
|
27
|
+
rendition. Asked only when the rendition list does not mark one
|
|
28
|
+
``original``; display names are configurable and translated.
|
|
29
|
+
download_timeout: Seconds a file download or a rendition may take.
|
|
30
|
+
rendition_poll_interval: First wait, in seconds, while FotoWare
|
|
31
|
+
prepares a rendition; it grows by half each time.
|
|
32
|
+
rendition_poll_interval_max: Longest wait between two polls.
|
|
33
|
+
query_size_budget: Largest estimated query size sent; see
|
|
34
|
+
fotoware_client.limits.
|
|
35
|
+
term_overhead: What a search term adds to FotoWare's translated query
|
|
36
|
+
besides its value; see fotoware_client.limits.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
oauth_token_path: str = "/fotoweb/oauth2/token"
|
|
40
|
+
api_descriptor_path: str = "/fotoweb/me"
|
|
41
|
+
original_rendition_name: str = "Original File"
|
|
42
|
+
download_timeout: int = 120
|
|
43
|
+
rendition_poll_interval: float = 0.1
|
|
44
|
+
rendition_poll_interval_max: float = 2.0
|
|
45
|
+
query_size_budget: int = QUERY_SIZE_BUDGET
|
|
46
|
+
term_overhead: int = TERM_OVERHEAD
|
|
47
|
+
|
|
48
|
+
def __post_init__(self) -> None:
|
|
49
|
+
if self.download_timeout <= 0:
|
|
50
|
+
raise ValueError("download_timeout must be positive")
|
|
51
|
+
if not 0 < self.rendition_poll_interval <= self.rendition_poll_interval_max:
|
|
52
|
+
raise ValueError("rendition poll intervals must satisfy 0 < first <= max")
|
|
53
|
+
if self.query_size_budget <= 0 or self.term_overhead < 0:
|
|
54
|
+
raise ValueError("query_size_budget must be positive, term_overhead >= 0")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-fotoware-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A thin, honest client for the FotoWare API: one call, one request; refuses queries FotoWare would answer wrongly; counts what each request costs
|
|
5
|
+
Author: Michael Hoppe
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/hoppefamily/python-fotoware-client
|
|
8
|
+
Project-URL: Source, https://github.com/hoppefamily/python-fotoware-client
|
|
9
|
+
Project-URL: Issues, https://github.com/hoppefamily/python-fotoware-client/issues
|
|
10
|
+
Keywords: fotoware,fotoweb,api,client
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: <3.15,>=3.14
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: requests>=2.31.0
|
|
19
|
+
Requires-Dist: urllib3<3,>=2.6.3
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# python-fotoware-client
|
|
23
|
+
|
|
24
|
+
[](https://github.com/hoppefamily/python-fotoware-client/actions/workflows/tests.yml)
|
|
25
|
+
[](https://github.com/hoppefamily/python-fotoware-client/actions/workflows/pre-commit.yml)
|
|
26
|
+
|
|
27
|
+
A thin, read-only Python client for the [FotoWare](https://www.fotoware.com/)
|
|
28
|
+
API (FotoWeb). It does what the API does and shows what that costs; it knows
|
|
29
|
+
nothing about any particular archive's fields.
|
|
30
|
+
|
|
31
|
+
Not affiliated with or endorsed by FotoWare AS. "FotoWare" and "FotoWeb" are
|
|
32
|
+
their trademarks.
|
|
33
|
+
|
|
34
|
+
## What it does
|
|
35
|
+
|
|
36
|
+
- **One call is one request.** `search_assets` returns one page; `next_page`
|
|
37
|
+
and `iter_search` follow `paging.next`. Nothing is batched or asked again
|
|
38
|
+
behind the caller's back. The only exception are the rendition downloads,
|
|
39
|
+
which poll until FotoWare has prepared the file.
|
|
40
|
+
- **Refuses a query FotoWare would answer wrongly.** Over FotoWare's size limit
|
|
41
|
+
a search comes back as a 500 or, worse, as an empty list that looks exactly
|
|
42
|
+
like "no hits". `search_assets` estimates the query first and raises
|
|
43
|
+
`QueryTooLargeError` over the budget, without sending it.
|
|
44
|
+
`estimate_query_size` is public, so callers can batch to stay under it.
|
|
45
|
+
- **Retries what is transient.** urllib3's `Retry` on the session retries 429
|
|
46
|
+
(honouring `Retry-After`) and 502/503/504, twice by default. A 500 is not
|
|
47
|
+
retried: it was always caused by the query.
|
|
48
|
+
- **Counts the cost.** `cost_snapshot()` reports requests, retries, errors,
|
|
49
|
+
search pages and empty search pages.
|
|
50
|
+
- OAuth client credentials with automatic token refresh; context manager.
|
|
51
|
+
- **Takes hrefs as FotoWare gives them.** Answers carry paths on the tenant;
|
|
52
|
+
every call that takes an href accepts a path or a full URL.
|
|
53
|
+
|
|
54
|
+
## FotoWare's limits
|
|
55
|
+
|
|
56
|
+
Measured on 2026-10-23 against FotoWeb 8.1.1223.0; details and edges in
|
|
57
|
+
`fotoware_client/limits.py`.
|
|
58
|
+
|
|
59
|
+
| Limit | Value |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| Assets per search answer | 25, with `pageSize` ignored. FotoWare guarantees no page size, so the client follows `paging.next` and counts nothing |
|
|
62
|
+
| Cost of an answer | ~0.4 s with hits whether 1 or 25 assets; 130–220 ms empty |
|
|
63
|
+
| Search size | about 2,000 by `estimate_query_size`; budget 1,500 |
|
|
64
|
+
|
|
65
|
+
So the cheapest search asks for as many terms as fit the budget and fills
|
|
66
|
+
whole pages.
|
|
67
|
+
|
|
68
|
+
Two search settings of an installation (Site Configuration › Searching) are out
|
|
69
|
+
of the client's sight:
|
|
70
|
+
|
|
71
|
+
- **Maximum number of hits** caps a result set. A search with more hits ends
|
|
72
|
+
early, without saying so.
|
|
73
|
+
- **Search string modification** decides how plain words without an operator
|
|
74
|
+
are combined (AND, OR or as a phrase). Write `AND`/`OR` explicitly, and the
|
|
75
|
+
setting does not matter.
|
|
76
|
+
|
|
77
|
+
## Settings of an installation
|
|
78
|
+
|
|
79
|
+
What a FotoWare installation may set differently, the client takes as
|
|
80
|
+
`ServerSettings`; the defaults are FotoWeb's:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from fotoware_client import FotoWareClient, ServerSettings
|
|
84
|
+
|
|
85
|
+
settings = ServerSettings(
|
|
86
|
+
original_rendition_name="Originaldatei", # display names are configurable
|
|
87
|
+
download_timeout=300, # seconds per download or rendition
|
|
88
|
+
rendition_poll_interval=0.5, # first wait while a rendition is made
|
|
89
|
+
query_size_budget=1200, # if the installation's limit is lower
|
|
90
|
+
)
|
|
91
|
+
client = FotoWareClient.from_env(settings=settings)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Also: `oauth_token_path`, `api_descriptor_path`, `rendition_poll_interval_max`
|
|
95
|
+
and `term_overhead`. See `fotoware_client/settings.py`.
|
|
96
|
+
|
|
97
|
+
## Credentials
|
|
98
|
+
|
|
99
|
+
Pass them to the constructor, or let `FotoWareClient.from_env()` find them:
|
|
100
|
+
|
|
101
|
+
1. the environment: `FOTOWARE_TENANT_URL`, `FOTOWARE_CLIENT_ID`,
|
|
102
|
+
`FOTOWARE_CLIENT_SECRET`;
|
|
103
|
+
2. else the credentials file `~/.config/fotoware/credentials`
|
|
104
|
+
(`$XDG_CONFIG_HOME/fotoware/credentials`), one `NAME=value` per line,
|
|
105
|
+
refused unless only its owner may read it (`chmod 600`).
|
|
106
|
+
|
|
107
|
+
Never on a command line, where a secret lands in the process list and the
|
|
108
|
+
shell history.
|
|
109
|
+
|
|
110
|
+
## Usage
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
from fotoware_client import FotoWareClient
|
|
114
|
+
|
|
115
|
+
with FotoWareClient.from_env() as client:
|
|
116
|
+
archive = next(
|
|
117
|
+
a for a in client.list_archives() if a.href.startswith("/fotoweb/archives/5000-")
|
|
118
|
+
)
|
|
119
|
+
query = "(631:4000000000001 OR 631:4000000000002) AND (dt:image OR dt:graphic)"
|
|
120
|
+
for page in client.iter_search(archive.href, query):
|
|
121
|
+
for asset in page.assets:
|
|
122
|
+
print(asset.filename)
|
|
123
|
+
print(client.cost_snapshot())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Other calls: `get_asset`, `get_field_labels`, `get_rendition_url`, `download_original`,
|
|
127
|
+
`download_original_to_file`, `download_rendition_to_file`, `download_bytes`.
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
All derive from `FotoWareError`:
|
|
132
|
+
|
|
133
|
+
| Error | When |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `QueryTooLargeError` | the query is over the size budget; nothing sent |
|
|
136
|
+
| `RateLimitError` | a 429 outlasted the retries |
|
|
137
|
+
| `RetryExhaustedError` | a 502/503/504, connection error or timeout outlasted the retries |
|
|
138
|
+
| `NotFoundError` | a 404 |
|
|
139
|
+
| `APIError` | any other error status, a 500 among them |
|
|
140
|
+
| `AuthenticationError` | the token could not be fetched |
|
|
141
|
+
| `ConfigurationError` | the client was set up wrongly |
|
|
142
|
+
|
|
143
|
+
## Install
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
pip install python-fotoware-client
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Development
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
make venv
|
|
153
|
+
make lint type test
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pre-commit hooks: `pip install pre-commit && pre-commit install`.
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
MIT, see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
fotoware_client/__init__.py,sha256=_8EgMpO6Ag_mvR0IsCYmakDraXOXRFttyDzoo5pPyZc,1497
|
|
2
|
+
fotoware_client/auth.py,sha256=_nTf51dx2fafwsbPVbEEAMvoOO8PSRvUV3lnQrS7NUs,10493
|
|
3
|
+
fotoware_client/client.py,sha256=Ii5JV3m0RN_XVPZUEe5OuG-S0Za7illVUOwCoAfq9J4,34886
|
|
4
|
+
fotoware_client/constants.py,sha256=0FaTPzxtJ-kl2OMnyujYp8LVqAEENFS6n5MnBGr21oo,663
|
|
5
|
+
fotoware_client/credentials.py,sha256=EeG-oDfg8mmospOz9y0mewOvl5oExQQVR0-AeZRm7Ew,2226
|
|
6
|
+
fotoware_client/exceptions.py,sha256=L9adqp2nyEBl7Iyn6fd7An1Xny-90atRES1_E7MFr28,2951
|
|
7
|
+
fotoware_client/limits.py,sha256=Y_G42GtX0uF-RtNQHkMvc5oRBZa5SwxllxbuPZG3VyQ,5255
|
|
8
|
+
fotoware_client/models.py,sha256=5N-jBxlznrI2g9cJorQ7qtNGNsniJzlYozWml0xeHFc,5172
|
|
9
|
+
fotoware_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
fotoware_client/retry.py,sha256=2eM86qGFvEsX745mMs0PBLmbQoENgTvie8uxutcz4MU,6489
|
|
11
|
+
fotoware_client/settings.py,sha256=Wqy1eCxDM5SdhDlnr5Q8qns9RJVRqBNlqohmhnIiusY,2517
|
|
12
|
+
python_fotoware_client-0.2.0.dist-info/licenses/LICENSE,sha256=qhRS3zWL3M_5Kh8lUh6xe-FIcZdWHcf7EliJG68Dw1U,1075
|
|
13
|
+
python_fotoware_client-0.2.0.dist-info/METADATA,sha256=NRec8xNrONn8Bvkwi8kQe9MpI0Ua8LE2LyrNP3BqFcM,6311
|
|
14
|
+
python_fotoware_client-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
python_fotoware_client-0.2.0.dist-info/top_level.txt,sha256=PIL52tp2K68xWUErUYhNkev0RYXHzue-EOZGLdk3sgM,16
|
|
16
|
+
python_fotoware_client-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Michael Hoppe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fotoware_client
|