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.
- pexafy-0.1.0/.github/workflows/ci.yml +22 -0
- pexafy-0.1.0/.gitignore +11 -0
- pexafy-0.1.0/CHANGELOG.md +11 -0
- pexafy-0.1.0/CONTRIBUTING.md +23 -0
- pexafy-0.1.0/LICENSE +21 -0
- pexafy-0.1.0/PKG-INFO +191 -0
- pexafy-0.1.0/README.md +161 -0
- pexafy-0.1.0/examples/async_batch.py +31 -0
- pexafy-0.1.0/examples/basic_search.py +18 -0
- pexafy-0.1.0/examples/download_a_set.py +32 -0
- pexafy-0.1.0/examples/match_an_image.py +25 -0
- pexafy-0.1.0/pyproject.toml +53 -0
- pexafy-0.1.0/src/pexafy/__init__.py +61 -0
- pexafy-0.1.0/src/pexafy/cli.py +89 -0
- pexafy-0.1.0/src/pexafy/client.py +447 -0
- pexafy-0.1.0/src/pexafy/errors.py +102 -0
- pexafy-0.1.0/src/pexafy/models.py +237 -0
- pexafy-0.1.0/tests/conftest.py +54 -0
- pexafy-0.1.0/tests/test_async.py +28 -0
- pexafy-0.1.0/tests/test_client.py +130 -0
- pexafy-0.1.0/tests/test_errors.py +80 -0
- pexafy-0.1.0/tests/test_models.py +32 -0
|
@@ -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
|
pexafy-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
]
|