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.
@@ -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
+ [![tests](https://github.com/hoppefamily/python-fotoware-client/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/hoppefamily/python-fotoware-client/actions/workflows/tests.yml)
25
+ [![pre-commit](https://github.com/hoppefamily/python-fotoware-client/actions/workflows/pre-commit.yml/badge.svg?branch=main)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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