python-fotoware-client 0.2.0__tar.gz

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.
Files changed (29) hide show
  1. python_fotoware_client-0.2.0/LICENSE +21 -0
  2. python_fotoware_client-0.2.0/PKG-INFO +160 -0
  3. python_fotoware_client-0.2.0/README.md +139 -0
  4. python_fotoware_client-0.2.0/pyproject.toml +57 -0
  5. python_fotoware_client-0.2.0/setup.cfg +4 -0
  6. python_fotoware_client-0.2.0/src/fotoware_client/__init__.py +62 -0
  7. python_fotoware_client-0.2.0/src/fotoware_client/auth.py +302 -0
  8. python_fotoware_client-0.2.0/src/fotoware_client/client.py +919 -0
  9. python_fotoware_client-0.2.0/src/fotoware_client/constants.py +13 -0
  10. python_fotoware_client-0.2.0/src/fotoware_client/credentials.py +59 -0
  11. python_fotoware_client-0.2.0/src/fotoware_client/exceptions.py +110 -0
  12. python_fotoware_client-0.2.0/src/fotoware_client/limits.py +133 -0
  13. python_fotoware_client-0.2.0/src/fotoware_client/models.py +190 -0
  14. python_fotoware_client-0.2.0/src/fotoware_client/py.typed +0 -0
  15. python_fotoware_client-0.2.0/src/fotoware_client/retry.py +166 -0
  16. python_fotoware_client-0.2.0/src/fotoware_client/settings.py +54 -0
  17. python_fotoware_client-0.2.0/src/python_fotoware_client.egg-info/PKG-INFO +160 -0
  18. python_fotoware_client-0.2.0/src/python_fotoware_client.egg-info/SOURCES.txt +27 -0
  19. python_fotoware_client-0.2.0/src/python_fotoware_client.egg-info/dependency_links.txt +1 -0
  20. python_fotoware_client-0.2.0/src/python_fotoware_client.egg-info/requires.txt +2 -0
  21. python_fotoware_client-0.2.0/src/python_fotoware_client.egg-info/top_level.txt +1 -0
  22. python_fotoware_client-0.2.0/tests/test_auth.py +269 -0
  23. python_fotoware_client-0.2.0/tests/test_client.py +510 -0
  24. python_fotoware_client-0.2.0/tests/test_credentials.py +81 -0
  25. python_fotoware_client-0.2.0/tests/test_exceptions.py +111 -0
  26. python_fotoware_client-0.2.0/tests/test_limits.py +54 -0
  27. python_fotoware_client-0.2.0/tests/test_models.py +208 -0
  28. python_fotoware_client-0.2.0/tests/test_retry.py +195 -0
  29. python_fotoware_client-0.2.0/tests/test_settings.py +103 -0
@@ -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,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,139 @@
1
+ # python-fotoware-client
2
+
3
+ [![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)
4
+ [![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)
5
+
6
+ A thin, read-only Python client for the [FotoWare](https://www.fotoware.com/)
7
+ API (FotoWeb). It does what the API does and shows what that costs; it knows
8
+ nothing about any particular archive's fields.
9
+
10
+ Not affiliated with or endorsed by FotoWare AS. "FotoWare" and "FotoWeb" are
11
+ their trademarks.
12
+
13
+ ## What it does
14
+
15
+ - **One call is one request.** `search_assets` returns one page; `next_page`
16
+ and `iter_search` follow `paging.next`. Nothing is batched or asked again
17
+ behind the caller's back. The only exception are the rendition downloads,
18
+ which poll until FotoWare has prepared the file.
19
+ - **Refuses a query FotoWare would answer wrongly.** Over FotoWare's size limit
20
+ a search comes back as a 500 or, worse, as an empty list that looks exactly
21
+ like "no hits". `search_assets` estimates the query first and raises
22
+ `QueryTooLargeError` over the budget, without sending it.
23
+ `estimate_query_size` is public, so callers can batch to stay under it.
24
+ - **Retries what is transient.** urllib3's `Retry` on the session retries 429
25
+ (honouring `Retry-After`) and 502/503/504, twice by default. A 500 is not
26
+ retried: it was always caused by the query.
27
+ - **Counts the cost.** `cost_snapshot()` reports requests, retries, errors,
28
+ search pages and empty search pages.
29
+ - OAuth client credentials with automatic token refresh; context manager.
30
+ - **Takes hrefs as FotoWare gives them.** Answers carry paths on the tenant;
31
+ every call that takes an href accepts a path or a full URL.
32
+
33
+ ## FotoWare's limits
34
+
35
+ Measured on 2026-10-23 against FotoWeb 8.1.1223.0; details and edges in
36
+ `fotoware_client/limits.py`.
37
+
38
+ | Limit | Value |
39
+ | --- | --- |
40
+ | Assets per search answer | 25, with `pageSize` ignored. FotoWare guarantees no page size, so the client follows `paging.next` and counts nothing |
41
+ | Cost of an answer | ~0.4 s with hits whether 1 or 25 assets; 130–220 ms empty |
42
+ | Search size | about 2,000 by `estimate_query_size`; budget 1,500 |
43
+
44
+ So the cheapest search asks for as many terms as fit the budget and fills
45
+ whole pages.
46
+
47
+ Two search settings of an installation (Site Configuration › Searching) are out
48
+ of the client's sight:
49
+
50
+ - **Maximum number of hits** caps a result set. A search with more hits ends
51
+ early, without saying so.
52
+ - **Search string modification** decides how plain words without an operator
53
+ are combined (AND, OR or as a phrase). Write `AND`/`OR` explicitly, and the
54
+ setting does not matter.
55
+
56
+ ## Settings of an installation
57
+
58
+ What a FotoWare installation may set differently, the client takes as
59
+ `ServerSettings`; the defaults are FotoWeb's:
60
+
61
+ ```python
62
+ from fotoware_client import FotoWareClient, ServerSettings
63
+
64
+ settings = ServerSettings(
65
+ original_rendition_name="Originaldatei", # display names are configurable
66
+ download_timeout=300, # seconds per download or rendition
67
+ rendition_poll_interval=0.5, # first wait while a rendition is made
68
+ query_size_budget=1200, # if the installation's limit is lower
69
+ )
70
+ client = FotoWareClient.from_env(settings=settings)
71
+ ```
72
+
73
+ Also: `oauth_token_path`, `api_descriptor_path`, `rendition_poll_interval_max`
74
+ and `term_overhead`. See `fotoware_client/settings.py`.
75
+
76
+ ## Credentials
77
+
78
+ Pass them to the constructor, or let `FotoWareClient.from_env()` find them:
79
+
80
+ 1. the environment: `FOTOWARE_TENANT_URL`, `FOTOWARE_CLIENT_ID`,
81
+ `FOTOWARE_CLIENT_SECRET`;
82
+ 2. else the credentials file `~/.config/fotoware/credentials`
83
+ (`$XDG_CONFIG_HOME/fotoware/credentials`), one `NAME=value` per line,
84
+ refused unless only its owner may read it (`chmod 600`).
85
+
86
+ Never on a command line, where a secret lands in the process list and the
87
+ shell history.
88
+
89
+ ## Usage
90
+
91
+ ```python
92
+ from fotoware_client import FotoWareClient
93
+
94
+ with FotoWareClient.from_env() as client:
95
+ archive = next(
96
+ a for a in client.list_archives() if a.href.startswith("/fotoweb/archives/5000-")
97
+ )
98
+ query = "(631:4000000000001 OR 631:4000000000002) AND (dt:image OR dt:graphic)"
99
+ for page in client.iter_search(archive.href, query):
100
+ for asset in page.assets:
101
+ print(asset.filename)
102
+ print(client.cost_snapshot())
103
+ ```
104
+
105
+ Other calls: `get_asset`, `get_field_labels`, `get_rendition_url`, `download_original`,
106
+ `download_original_to_file`, `download_rendition_to_file`, `download_bytes`.
107
+
108
+ ## Errors
109
+
110
+ All derive from `FotoWareError`:
111
+
112
+ | Error | When |
113
+ | --- | --- |
114
+ | `QueryTooLargeError` | the query is over the size budget; nothing sent |
115
+ | `RateLimitError` | a 429 outlasted the retries |
116
+ | `RetryExhaustedError` | a 502/503/504, connection error or timeout outlasted the retries |
117
+ | `NotFoundError` | a 404 |
118
+ | `APIError` | any other error status, a 500 among them |
119
+ | `AuthenticationError` | the token could not be fetched |
120
+ | `ConfigurationError` | the client was set up wrongly |
121
+
122
+ ## Install
123
+
124
+ ```bash
125
+ pip install python-fotoware-client
126
+ ```
127
+
128
+ ## Development
129
+
130
+ ```bash
131
+ make venv
132
+ make lint type test
133
+ ```
134
+
135
+ Pre-commit hooks: `pip install pre-commit && pre-commit install`.
136
+
137
+ ## License
138
+
139
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "python-fotoware-client"
7
+ version = "0.2.0"
8
+ description = "A thin, honest client for the FotoWare API: one call, one request; refuses queries FotoWare would answer wrongly; counts what each request costs"
9
+ readme = "README.md"
10
+ requires-python = ">=3.14,<3.15"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ keywords = ["fotoware", "fotoweb", "api", "client"]
14
+ authors = [{ name = "Michael Hoppe" }]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "requests>=2.31.0",
23
+ "urllib3>=2.6.3,<3",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/hoppefamily/python-fotoware-client"
28
+ Source = "https://github.com/hoppefamily/python-fotoware-client"
29
+ Issues = "https://github.com/hoppefamily/python-fotoware-client/issues"
30
+
31
+ [tool.setuptools]
32
+ package-dir = {"" = "src"}
33
+
34
+ [tool.setuptools.packages.find]
35
+ where = ["src"]
36
+
37
+ [tool.setuptools.package-data]
38
+ fotoware_client = ["py.typed"]
39
+
40
+ [tool.vulture]
41
+ # src/ and any scripts, deliberately without tests/: a function whose only
42
+ # caller is its own test then has no caller at all and gets reported. With
43
+ # tests/ in this list it would look used, which is how such code survives --
44
+ # coverage counts a test as an execution, so it even reads as fully covered.
45
+ # Add every other directory that holds callers (scripts/, utility/, ...), or
46
+ # their code will be reported as dead.
47
+ paths = ["src", "vulture_whitelist.py"]
48
+ # 60, because that is where vulture reports an unused *function*. At 80 it
49
+ # only states what it is certain of -- unused imports, unreachable code, dead
50
+ # variables -- and ruff already catches most of that, so a gate at 80 lets an
51
+ # uncalled function straight through. Tried and measured, not assumed.
52
+ #
53
+ # 60 also guesses. In a repo that starts here the guesses are few; in one that
54
+ # adopts this later, run `vulture --make-whitelist` once and commit the result
55
+ # as a baseline, so the gate speaks only about code written from then on.
56
+ min_confidence = 60
57
+ sort_by_size = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,62 @@
1
+ """FotoWare API Client - Layer 1: Pure API."""
2
+
3
+ from fotoware_client.auth import OAuthAuthenticator, OAuthToken
4
+ from fotoware_client.client import FotoWareClient
5
+ from fotoware_client.constants import DEFAULT_TIMEOUT
6
+ from fotoware_client.credentials import credentials_file, load_credentials
7
+ from fotoware_client.exceptions import (
8
+ APIError,
9
+ AuthenticationError,
10
+ ConfigurationError,
11
+ FotoWareError,
12
+ NotFoundError,
13
+ RateLimitError,
14
+ RetryExhaustedError,
15
+ )
16
+ from fotoware_client.limits import (
17
+ QUERY_SIZE_BUDGET,
18
+ QUERY_SIZE_LIMIT,
19
+ QueryTooLargeError,
20
+ estimate_query_size,
21
+ )
22
+ from fotoware_client.models import Archive, Asset, SearchResult
23
+ from fotoware_client.retry import NO_RETRY, STANDARD_RETRY, RetryConfig
24
+ from fotoware_client.settings import ServerSettings
25
+
26
+ __version__ = "0.2.0"
27
+
28
+ __all__ = [
29
+ # Client
30
+ "FotoWareClient",
31
+ # Models
32
+ "Archive",
33
+ "Asset",
34
+ "SearchResult",
35
+ # Auth
36
+ "OAuthAuthenticator",
37
+ "OAuthToken",
38
+ # Exceptions
39
+ "FotoWareError",
40
+ "AuthenticationError",
41
+ "RateLimitError",
42
+ "NotFoundError",
43
+ "APIError",
44
+ "ConfigurationError",
45
+ "RetryExhaustedError",
46
+ "QueryTooLargeError",
47
+ # Installation
48
+ "ServerSettings",
49
+ # Credentials
50
+ "credentials_file",
51
+ "load_credentials",
52
+ # Limits
53
+ "QUERY_SIZE_BUDGET",
54
+ "QUERY_SIZE_LIMIT",
55
+ "estimate_query_size",
56
+ # Retry
57
+ "RetryConfig",
58
+ "STANDARD_RETRY",
59
+ "NO_RETRY",
60
+ # Constants
61
+ "DEFAULT_TIMEOUT",
62
+ ]