pexafy 0.1.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.
@@ -0,0 +1,22 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.10", "3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python }}
19
+ cache: pip
20
+ - run: pip install -e ".[dev]"
21
+ - run: ruff check .
22
+ - run: pytest -q
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ .env
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ - Synchronous and asynchronous clients
8
+ - Text search, image search, similar photos, facets, collections, usage
9
+ - Cursor paging handled by `iter_search`
10
+ - Typed exceptions, retries on timeouts and 5xx, `Retry-After` honoured
11
+ - `pexafy` command line client
@@ -0,0 +1,23 @@
1
+ # Contributing
2
+
3
+ Bug reports and pull requests are welcome.
4
+
5
+ ## Reporting a bug
6
+
7
+ Open an issue with what you ran, what you expected, and what happened instead.
8
+ A minimal reproduction saves a lot of back and forth.
9
+
10
+ ## Pull requests
11
+
12
+ - Keep the change focused — one concern per PR.
13
+ - Add a test if you are fixing a bug or adding behaviour.
14
+ - `ruff check .` and `pytest` should pass before you push.
15
+
16
+ ## Development
17
+
18
+ ```bash
19
+ git clone https://github.com/Pexafy/pexafy-python.git
20
+ cd pexafy-python
21
+ pip install -e ".[dev]"
22
+ pytest
23
+ ```
pexafy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marouane Tijani
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.
pexafy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.5
2
+ Name: pexafy
3
+ Version: 0.1.0
4
+ Summary: Python client for the Pexafy stock photo search API
5
+ Project-URL: Homepage, https://pexafy.com
6
+ Project-URL: Documentation, https://docs.pexafy.com
7
+ Project-URL: Source, https://github.com/Pexafy/pexafy-python
8
+ Project-URL: Issues, https://github.com/Pexafy/pexafy-python/issues
9
+ Author-email: Marouane Tijani <marouane@pexafy.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api client,image search,semantic search,stock photos
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Graphics
22
+ Requires-Python: >=3.9
23
+ Requires-Dist: httpx>=0.24
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Requires-Dist: respx>=0.20; extra == 'dev'
28
+ Requires-Dist: ruff>=0.5; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pexafy-python
32
+
33
+ Python client for the [Pexafy](https://pexafy.com) image search API. Search a
34
+ catalogue of free stock photos by describing what you want, or by handing it an
35
+ image to match.
36
+
37
+ ```bash
38
+ pip install pexafy
39
+ ```
40
+
41
+ ## Getting started
42
+
43
+ ```python
44
+ from pexafy import Client
45
+
46
+ client = Client("your-api-key")
47
+
48
+ for photo in client.search("a quiet street in the rain", per_page=5):
49
+ print(photo.urls.regular, "-", photo.alt_text)
50
+ ```
51
+
52
+ The key comes from your [dashboard](https://pexafy.com/dashboard/); the free
53
+ tier does not ask for a card. If you would rather not put it in the code, the
54
+ client picks up `PEXAFY_API_KEY` from the environment.
55
+
56
+ ## Writing queries
57
+
58
+ Search runs on meaning, not keywords, so full sentences work better than a pile
59
+ of nouns. `two people hiking on a ridge at dawn` finds what you would expect;
60
+ `hiking dawn people` gives you a worse ranking, because you have thrown away
61
+ the relationships between the words.
62
+
63
+ Filters narrow the result set after the semantic match:
64
+
65
+ ```python
66
+ result = client.search(
67
+ "an empty office at night",
68
+ orientation="landscape",
69
+ color_name="blue",
70
+ source=["Pexels", "Unsplash"],
71
+ per_page=20,
72
+ )
73
+
74
+ print(len(result), "photos in", result.took_ms, "ms")
75
+ ```
76
+
77
+ List filters accept either a list or a comma separated string.
78
+
79
+ ## Paging
80
+
81
+ A single call returns one page. `iter_search` follows the cursor for you and
82
+ yields photos until the results run out or you have seen enough:
83
+
84
+ ```python
85
+ for photo in client.iter_search("vintage typewriter", max_results=200):
86
+ download(photo.urls.large)
87
+ ```
88
+
89
+ ## Search by image
90
+
91
+ Pass a path, raw bytes, or an open file:
92
+
93
+ ```python
94
+ similar = client.search_by_image("moodboard/reference.jpg", per_page=12)
95
+ ```
96
+
97
+ If you already have a photo id, `client.similar(photo_id)` is cheaper — the
98
+ image does not have to be uploaded and encoded again.
99
+
100
+ ## Async
101
+
102
+ The same surface, awaitable:
103
+
104
+ ```python
105
+ import asyncio
106
+ from pexafy import AsyncClient
107
+
108
+ async def main():
109
+ async with AsyncClient() as client:
110
+ pages = await asyncio.gather(
111
+ client.search("desert road"),
112
+ client.search("snow covered pines"),
113
+ )
114
+ for page in pages:
115
+ print(len(page))
116
+
117
+ asyncio.run(main())
118
+ ```
119
+
120
+ ## Errors
121
+
122
+ Everything raised inherits from `PexafyError`. The ones worth catching
123
+ separately:
124
+
125
+ ```python
126
+ from pexafy import errors
127
+
128
+ try:
129
+ client.search("...")
130
+ except errors.RateLimitError as exc:
131
+ time.sleep(exc.retry_after or 60)
132
+ except errors.AuthenticationError:
133
+ ... # key is missing, malformed or revoked
134
+ except errors.APIError as exc:
135
+ print(exc.status_code, exc.code, exc.request_id)
136
+ ```
137
+
138
+ `request_id` is worth logging. It is the fastest way to get an answer if you
139
+ need to ask about a specific call.
140
+
141
+ Timeouts and 5xx responses are retried twice with backoff, and `Retry-After` is
142
+ honoured when the server sends it. Set `max_retries=0` if you would rather
143
+ handle that yourself.
144
+
145
+ ## Command line
146
+
147
+ ```
148
+ $ pexafy search "morning fog over pine trees" -n 3
149
+ 0.847 019e0eb8-b028-73cb-9296-dfa70f557bc9 4000x2667 green Pexels https://...
150
+ 0.812 019e4c9b-3022-7660-b43d-e730b8435f24 6000x4000 green Unsplash https://...
151
+ 0.798 019e4f39-66d4-7ef2-bc9b-eb5340fd243e 3648x5472 grey Pexels https://...
152
+ ```
153
+
154
+ `pexafy photo <id>`, `pexafy similar <id>` and `pexafy usage` are also there.
155
+ Add `--json` to any of them for the raw response.
156
+
157
+ ## Attribution
158
+
159
+ Photos come from several providers with different licence terms. Every photo
160
+ carries an `attribution` object with a ready made credit line:
161
+
162
+ ```python
163
+ photo.attribution.plain # Photo by J. Doe
164
+ photo.attribution.html # <a href="...">J. Doe</a>
165
+ ```
166
+
167
+ Check `photo.license_type` if your use depends on it.
168
+
169
+ ## Reference
170
+
171
+ | Method | What it does |
172
+ | --- | --- |
173
+ | `search(q, **filters)` | one page of results |
174
+ | `iter_search(q, max_results=None, **filters)` | every page, cursor handled |
175
+ | `search_by_image(image, **filters)` | match an image you supply |
176
+ | `get_photo(photo_id)` | one photo by id |
177
+ | `similar(photo_id, **filters)` | photos close to an existing one |
178
+ | `colors()` `sources()` `orientations()` `licenses()` | filter values you can use |
179
+ | `suggest_photographers(q)` `photographer(username)` | photographer lookup |
180
+ | `collections()` `create_collection(name)` `add_to_collection(id, photo_id)` | saved sets |
181
+ | `usage()` `usage_daily()` `usage_monthly()` `usage_by_key()` | where you are against your quota |
182
+
183
+ Full API documentation is at [docs.pexafy.com](https://docs.pexafy.com).
184
+
185
+ ## Requirements
186
+
187
+ Python 3.9 or newer. The only dependency is `httpx`.
188
+
189
+ ## Licence
190
+
191
+ MIT.
pexafy-0.1.0/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # pexafy-python
2
+
3
+ Python client for the [Pexafy](https://pexafy.com) image search API. Search a
4
+ catalogue of free stock photos by describing what you want, or by handing it an
5
+ image to match.
6
+
7
+ ```bash
8
+ pip install pexafy
9
+ ```
10
+
11
+ ## Getting started
12
+
13
+ ```python
14
+ from pexafy import Client
15
+
16
+ client = Client("your-api-key")
17
+
18
+ for photo in client.search("a quiet street in the rain", per_page=5):
19
+ print(photo.urls.regular, "-", photo.alt_text)
20
+ ```
21
+
22
+ The key comes from your [dashboard](https://pexafy.com/dashboard/); the free
23
+ tier does not ask for a card. If you would rather not put it in the code, the
24
+ client picks up `PEXAFY_API_KEY` from the environment.
25
+
26
+ ## Writing queries
27
+
28
+ Search runs on meaning, not keywords, so full sentences work better than a pile
29
+ of nouns. `two people hiking on a ridge at dawn` finds what you would expect;
30
+ `hiking dawn people` gives you a worse ranking, because you have thrown away
31
+ the relationships between the words.
32
+
33
+ Filters narrow the result set after the semantic match:
34
+
35
+ ```python
36
+ result = client.search(
37
+ "an empty office at night",
38
+ orientation="landscape",
39
+ color_name="blue",
40
+ source=["Pexels", "Unsplash"],
41
+ per_page=20,
42
+ )
43
+
44
+ print(len(result), "photos in", result.took_ms, "ms")
45
+ ```
46
+
47
+ List filters accept either a list or a comma separated string.
48
+
49
+ ## Paging
50
+
51
+ A single call returns one page. `iter_search` follows the cursor for you and
52
+ yields photos until the results run out or you have seen enough:
53
+
54
+ ```python
55
+ for photo in client.iter_search("vintage typewriter", max_results=200):
56
+ download(photo.urls.large)
57
+ ```
58
+
59
+ ## Search by image
60
+
61
+ Pass a path, raw bytes, or an open file:
62
+
63
+ ```python
64
+ similar = client.search_by_image("moodboard/reference.jpg", per_page=12)
65
+ ```
66
+
67
+ If you already have a photo id, `client.similar(photo_id)` is cheaper — the
68
+ image does not have to be uploaded and encoded again.
69
+
70
+ ## Async
71
+
72
+ The same surface, awaitable:
73
+
74
+ ```python
75
+ import asyncio
76
+ from pexafy import AsyncClient
77
+
78
+ async def main():
79
+ async with AsyncClient() as client:
80
+ pages = await asyncio.gather(
81
+ client.search("desert road"),
82
+ client.search("snow covered pines"),
83
+ )
84
+ for page in pages:
85
+ print(len(page))
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ ## Errors
91
+
92
+ Everything raised inherits from `PexafyError`. The ones worth catching
93
+ separately:
94
+
95
+ ```python
96
+ from pexafy import errors
97
+
98
+ try:
99
+ client.search("...")
100
+ except errors.RateLimitError as exc:
101
+ time.sleep(exc.retry_after or 60)
102
+ except errors.AuthenticationError:
103
+ ... # key is missing, malformed or revoked
104
+ except errors.APIError as exc:
105
+ print(exc.status_code, exc.code, exc.request_id)
106
+ ```
107
+
108
+ `request_id` is worth logging. It is the fastest way to get an answer if you
109
+ need to ask about a specific call.
110
+
111
+ Timeouts and 5xx responses are retried twice with backoff, and `Retry-After` is
112
+ honoured when the server sends it. Set `max_retries=0` if you would rather
113
+ handle that yourself.
114
+
115
+ ## Command line
116
+
117
+ ```
118
+ $ pexafy search "morning fog over pine trees" -n 3
119
+ 0.847 019e0eb8-b028-73cb-9296-dfa70f557bc9 4000x2667 green Pexels https://...
120
+ 0.812 019e4c9b-3022-7660-b43d-e730b8435f24 6000x4000 green Unsplash https://...
121
+ 0.798 019e4f39-66d4-7ef2-bc9b-eb5340fd243e 3648x5472 grey Pexels https://...
122
+ ```
123
+
124
+ `pexafy photo <id>`, `pexafy similar <id>` and `pexafy usage` are also there.
125
+ Add `--json` to any of them for the raw response.
126
+
127
+ ## Attribution
128
+
129
+ Photos come from several providers with different licence terms. Every photo
130
+ carries an `attribution` object with a ready made credit line:
131
+
132
+ ```python
133
+ photo.attribution.plain # Photo by J. Doe
134
+ photo.attribution.html # <a href="...">J. Doe</a>
135
+ ```
136
+
137
+ Check `photo.license_type` if your use depends on it.
138
+
139
+ ## Reference
140
+
141
+ | Method | What it does |
142
+ | --- | --- |
143
+ | `search(q, **filters)` | one page of results |
144
+ | `iter_search(q, max_results=None, **filters)` | every page, cursor handled |
145
+ | `search_by_image(image, **filters)` | match an image you supply |
146
+ | `get_photo(photo_id)` | one photo by id |
147
+ | `similar(photo_id, **filters)` | photos close to an existing one |
148
+ | `colors()` `sources()` `orientations()` `licenses()` | filter values you can use |
149
+ | `suggest_photographers(q)` `photographer(username)` | photographer lookup |
150
+ | `collections()` `create_collection(name)` `add_to_collection(id, photo_id)` | saved sets |
151
+ | `usage()` `usage_daily()` `usage_monthly()` `usage_by_key()` | where you are against your quota |
152
+
153
+ Full API documentation is at [docs.pexafy.com](https://docs.pexafy.com).
154
+
155
+ ## Requirements
156
+
157
+ Python 3.9 or newer. The only dependency is `httpx`.
158
+
159
+ ## Licence
160
+
161
+ MIT.
@@ -0,0 +1,31 @@
1
+ """Run several searches at once.
2
+
3
+ Filling one page per section of a document is the common case, and there is no
4
+ reason to do it serially.
5
+
6
+ PEXAFY_API_KEY=... python async_batch.py
7
+ """
8
+
9
+ import asyncio
10
+
11
+ from pexafy import AsyncClient
12
+
13
+ SECTIONS = [
14
+ "an empty office at night",
15
+ "hands typing on a mechanical keyboard",
16
+ "a server room seen from the doorway",
17
+ "someone reading on a train",
18
+ ]
19
+
20
+
21
+ async def main():
22
+ async with AsyncClient() as client:
23
+ pages = await asyncio.gather(*(client.search(q, per_page=3) for q in SECTIONS))
24
+
25
+ for query, page in zip(SECTIONS, pages):
26
+ print(f"\n{query}")
27
+ for photo in page:
28
+ print(f" {photo.urls.small}")
29
+
30
+
31
+ asyncio.run(main())
@@ -0,0 +1,18 @@
1
+ """The smallest useful thing: search and print what came back.
2
+
3
+ PEXAFY_API_KEY=... python basic_search.py "a quiet street in the rain"
4
+ """
5
+
6
+ import sys
7
+
8
+ from pexafy import Client
9
+
10
+ query = sys.argv[1] if len(sys.argv) > 1 else "a quiet street in the rain"
11
+
12
+ with Client() as client:
13
+ result = client.search(query, per_page=5)
14
+ print(f"{len(result)} photos in {result.took_ms:.0f} ms\n")
15
+ for photo in result:
16
+ print(f"{photo.relevance_score:.3f} {photo.alt_text}")
17
+ print(f" {photo.urls.regular}")
18
+ print(f" {photo.attribution.plain}\n")
@@ -0,0 +1,32 @@
1
+ """Page through results and save the files.
2
+
3
+ Shows the two things that matter when you pull more than one page: let
4
+ iter_search follow the cursor, and stop on your own terms rather than draining
5
+ the whole catalogue.
6
+
7
+ PEXAFY_API_KEY=... python download_a_set.py "vintage typewriter" 25
8
+ """
9
+
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import httpx
14
+
15
+ from pexafy import Client, errors
16
+
17
+ query = sys.argv[1] if len(sys.argv) > 1 else "vintage typewriter"
18
+ count = int(sys.argv[2]) if len(sys.argv) > 2 else 25
19
+
20
+ out = Path("downloads")
21
+ out.mkdir(exist_ok=True)
22
+
23
+ with Client() as client, httpx.Client(timeout=60) as http:
24
+ try:
25
+ for photo in client.iter_search(query, max_results=count, orientation="landscape"):
26
+ target = out / f"{photo.photo_id}.jpg"
27
+ if target.exists():
28
+ continue
29
+ target.write_bytes(http.get(photo.urls.large).content)
30
+ print(f"saved {target} ({photo.attribution.plain})")
31
+ except errors.RateLimitError as exc:
32
+ print(f"quota reached, retry in {exc.retry_after or 60:.0f}s")
@@ -0,0 +1,25 @@
1
+ """Find photos that look like one you already have.
2
+
3
+ Useful for building a moodboard around a reference, or finding an alternative
4
+ to an image you cannot license.
5
+
6
+ PEXAFY_API_KEY=... python match_an_image.py reference.jpg
7
+ """
8
+
9
+ import sys
10
+
11
+ from pexafy import Client
12
+
13
+ reference = sys.argv[1]
14
+
15
+ with Client() as client:
16
+ result = client.search_by_image(reference, per_page=8)
17
+
18
+ for photo in result:
19
+ print(f"{photo.relevance_score:.3f} {photo.color_name:<8} {photo.urls.small}")
20
+
21
+ # If you plan to keep going from one of these, use its id rather than
22
+ # uploading the image again — no encoding round trip.
23
+ if len(result):
24
+ more = client.similar(result[0].photo_id, per_page=8)
25
+ print(f"\n{len(more)} more like the top result")
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pexafy"
7
+ version = "0.1.0"
8
+ description = "Python client for the Pexafy stock photo search API"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{name = "Marouane Tijani", email = "marouane@pexafy.com"}]
13
+ keywords = ["stock photos", "image search", "semantic search", "api client"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3.9",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Multimedia :: Graphics",
24
+ ]
25
+ dependencies = ["httpx>=0.24"]
26
+
27
+ [project.optional-dependencies]
28
+ dev = ["pytest>=7", "pytest-asyncio>=0.21", "respx>=0.20", "ruff>=0.5"]
29
+
30
+ [project.urls]
31
+ Homepage = "https://pexafy.com"
32
+ Documentation = "https://docs.pexafy.com"
33
+ Source = "https://github.com/Pexafy/pexafy-python"
34
+ Issues = "https://github.com/Pexafy/pexafy-python/issues"
35
+
36
+ [project.scripts]
37
+ pexafy = "pexafy.cli:main"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/pexafy"]
41
+
42
+ [tool.ruff]
43
+ line-length = 100
44
+ target-version = "py39"
45
+
46
+ [tool.ruff.lint]
47
+ select = ["E", "F", "I", "UP", "B"]
48
+ # Optional[...] rather than `| None` — the package still supports 3.9, and the
49
+ # postponed-annotation trick is easy to break by accident in a dataclass.
50
+ ignore = ["UP007", "UP045"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
@@ -0,0 +1,61 @@
1
+ """Python client for the Pexafy image search API.
2
+
3
+ from pexafy import Client
4
+
5
+ client = Client("your-api-key")
6
+ for photo in client.search("a quiet street in the rain", per_page=10):
7
+ print(photo.urls.regular)
8
+
9
+ Get a key at https://pexafy.com — the free tier does not need a card.
10
+ """
11
+
12
+ __version__ = "0.1.0"
13
+
14
+ from .client import DEFAULT_BASE_URL, AsyncClient, Client
15
+ from .errors import (
16
+ APIError,
17
+ AuthenticationError,
18
+ ConnectionError_,
19
+ NotFoundError,
20
+ PermissionError_,
21
+ PexafyError,
22
+ RateLimitError,
23
+ ServerError,
24
+ TimeoutError_,
25
+ ValidationError,
26
+ )
27
+ from .models import (
28
+ Attribution,
29
+ Collection,
30
+ CollectionItem,
31
+ Pagination,
32
+ Photo,
33
+ Photographer,
34
+ PhotoUrls,
35
+ SearchResult,
36
+ )
37
+
38
+ __all__ = [
39
+ "Client",
40
+ "AsyncClient",
41
+ "DEFAULT_BASE_URL",
42
+ "Photo",
43
+ "PhotoUrls",
44
+ "Attribution",
45
+ "Photographer",
46
+ "Collection",
47
+ "CollectionItem",
48
+ "Pagination",
49
+ "SearchResult",
50
+ "PexafyError",
51
+ "APIError",
52
+ "AuthenticationError",
53
+ "PermissionError_",
54
+ "NotFoundError",
55
+ "RateLimitError",
56
+ "ValidationError",
57
+ "ServerError",
58
+ "TimeoutError_",
59
+ "ConnectionError_",
60
+ "__version__",
61
+ ]