jouyaar 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.
- jouyaar-0.1.0/.github/workflows/ci.yml +35 -0
- jouyaar-0.1.0/.github/workflows/release.yml +21 -0
- jouyaar-0.1.0/.gitignore +9 -0
- jouyaar-0.1.0/CHANGELOG.md +14 -0
- jouyaar-0.1.0/LICENSE +21 -0
- jouyaar-0.1.0/PKG-INFO +115 -0
- jouyaar-0.1.0/README.md +95 -0
- jouyaar-0.1.0/examples/quickstart.py +35 -0
- jouyaar-0.1.0/pyproject.toml +47 -0
- jouyaar-0.1.0/src/jouyaar/__init__.py +51 -0
- jouyaar-0.1.0/src/jouyaar/_core.py +92 -0
- jouyaar-0.1.0/src/jouyaar/_version.py +1 -0
- jouyaar-0.1.0/src/jouyaar/client.py +157 -0
- jouyaar-0.1.0/src/jouyaar/errors.py +68 -0
- jouyaar-0.1.0/src/jouyaar/models.py +113 -0
- jouyaar-0.1.0/src/jouyaar/py.typed +0 -0
- jouyaar-0.1.0/tests/test_client.py +145 -0
- jouyaar-0.1.0/uv.lock +585 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
release:
|
|
8
|
+
types: [published]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
matrix:
|
|
15
|
+
python-version: ["3.9", "3.11", "3.13"]
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
- run: uv sync --extra dev
|
|
20
|
+
- run: uv run ruff check src tests
|
|
21
|
+
- run: uv run pytest -q
|
|
22
|
+
|
|
23
|
+
publish:
|
|
24
|
+
# Publish to PyPI on a GitHub Release, via Trusted Publishing (no token needed).
|
|
25
|
+
needs: test
|
|
26
|
+
if: github.event_name == 'release'
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/checkout@v4
|
|
33
|
+
- uses: astral-sh/setup-uv@v5
|
|
34
|
+
- run: uv build
|
|
35
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
release:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
with:
|
|
15
|
+
fetch-depth: 0
|
|
16
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
17
|
+
|
|
18
|
+
- name: Python Semantic Versioning
|
|
19
|
+
uses: mostafaghadimi/python-semantic-versioning@v2.8.1
|
|
20
|
+
with:
|
|
21
|
+
gh_token: ${{ secrets.GITHUB_TOKEN }}
|
jouyaar-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CHANGELOG
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## v0.1.0 (2026-08-26)
|
|
5
|
+
|
|
6
|
+
### Ci
|
|
7
|
+
|
|
8
|
+
- Add release workflow
|
|
9
|
+
([`e9ef9ac`](https://github.com/jouyaar/jouyaar-python-sdk/commit/e9ef9ac28fc8748a2411681926bfda0eef290ecb))
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Initial release — Jooyar Python SDK v0.1.0
|
|
14
|
+
([`26a7ba2`](https://github.com/jouyaar/jouyaar-python-sdk/commit/26a7ba296d7551324ac8d9080b4c2766c3231844))
|
jouyaar-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jooyar
|
|
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.
|
jouyaar-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jouyaar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Jooyar (جویار) agentic search API
|
|
5
|
+
Project-URL: Homepage, https://developers.jouyaar.ir
|
|
6
|
+
Project-URL: Documentation, https://developers.jouyaar.ir
|
|
7
|
+
Author: Jooyar
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: api,iran,jooyar,jouyaar,sdk,search
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Requires-Dist: httpx>=0.27
|
|
13
|
+
Requires-Dist: pydantic>=2.7
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
17
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Jooyar Python SDK
|
|
22
|
+
|
|
23
|
+
Official Python client for the **Jooyar (جویار)** agentic search API — compare flights, lodging,
|
|
24
|
+
bus, train, retail, and internet plans across Iranian providers from one call.
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install jouyaar
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from jouyaar import Jooyaar
|
|
34
|
+
|
|
35
|
+
client = Jooyaar(api_key="sk_live_…") # or set JOUYAAR_API_KEY
|
|
36
|
+
|
|
37
|
+
# Structured search — deterministic, LLM-free, cheapest:
|
|
38
|
+
res = client.search(
|
|
39
|
+
category="flight",
|
|
40
|
+
params={"origin": "THR", "destination": "MHD", "departure_date": "1404-06-10", "passengers": 1},
|
|
41
|
+
sort="price",
|
|
42
|
+
)
|
|
43
|
+
for q in res.quotes:
|
|
44
|
+
print(q.provider, f"{q.price_toman:,} تومان", q.plan_name)
|
|
45
|
+
|
|
46
|
+
# Natural-language search — the API extracts the parameters for you:
|
|
47
|
+
res = client.search(category="flight", prompt="ارزانترین پرواز تهران به مشهد فردا صبح")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Get a key at **[developers.jouyaar.ir](https://developers.jouyaar.ir)**.
|
|
51
|
+
|
|
52
|
+
## Two ways to search
|
|
53
|
+
|
|
54
|
+
| Mode | How | When |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `params=` | structured fields (see `client.categories()`) | you already know the fields — deterministic, no LLM, cheapest |
|
|
57
|
+
| `prompt=` | free-text intent | let the API parse a natural-language request |
|
|
58
|
+
|
|
59
|
+
Pass exactly one. `client.categories()` lists every category and its required/optional fields.
|
|
60
|
+
|
|
61
|
+
## Async
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from jouyaar import AsyncJooyaar
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
async with AsyncJooyaar() as client:
|
|
69
|
+
res = await client.search(category="lodging",
|
|
70
|
+
params={"city": "تهران", "checkin": "1404-06-10", "checkout": "1404-06-12"})
|
|
71
|
+
print(len(res.quotes), "results")
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Errors
|
|
77
|
+
|
|
78
|
+
All failures raise a `JooyaarError` subclass carrying the server `code` and `request_id`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
import time
|
|
82
|
+
from jouyaar import Jooyaar, RateLimitError, QuotaExceededError, AuthenticationError
|
|
83
|
+
|
|
84
|
+
client = Jooyaar()
|
|
85
|
+
try:
|
|
86
|
+
res = client.search(category="flight", prompt="…")
|
|
87
|
+
except RateLimitError as e:
|
|
88
|
+
time.sleep(e.retry_after or 1) # per-minute limit — back off and retry
|
|
89
|
+
except QuotaExceededError:
|
|
90
|
+
... # monthly quota spent — upgrade the plan
|
|
91
|
+
except AuthenticationError:
|
|
92
|
+
... # bad/revoked key
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`RateLimitError` (429, per-minute) and 5xx responses are **retried automatically** with backoff
|
|
96
|
+
(honoring `Retry-After`); 4xx are not. Tune with `Jooyaar(max_retries=..., timeout=...)`.
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
| Argument | Env var | Default |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `api_key` | `JOUYAAR_API_KEY` | — (required) |
|
|
103
|
+
| `base_url` | `JOUYAAR_BASE_URL` | `https://api.jouyaar.ir` |
|
|
104
|
+
| `timeout` | — | `30.0` |
|
|
105
|
+
| `max_retries` | — | `2` |
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
uv sync --extra dev
|
|
111
|
+
uv run pytest
|
|
112
|
+
uv run ruff check src tests
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
MIT licensed.
|
jouyaar-0.1.0/README.md
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Jooyar Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the **Jooyar (جویار)** agentic search API — compare flights, lodging,
|
|
4
|
+
bus, train, retail, and internet plans across Iranian providers from one call.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install jouyaar
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from jouyaar import Jooyaar
|
|
14
|
+
|
|
15
|
+
client = Jooyaar(api_key="sk_live_…") # or set JOUYAAR_API_KEY
|
|
16
|
+
|
|
17
|
+
# Structured search — deterministic, LLM-free, cheapest:
|
|
18
|
+
res = client.search(
|
|
19
|
+
category="flight",
|
|
20
|
+
params={"origin": "THR", "destination": "MHD", "departure_date": "1404-06-10", "passengers": 1},
|
|
21
|
+
sort="price",
|
|
22
|
+
)
|
|
23
|
+
for q in res.quotes:
|
|
24
|
+
print(q.provider, f"{q.price_toman:,} تومان", q.plan_name)
|
|
25
|
+
|
|
26
|
+
# Natural-language search — the API extracts the parameters for you:
|
|
27
|
+
res = client.search(category="flight", prompt="ارزانترین پرواز تهران به مشهد فردا صبح")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Get a key at **[developers.jouyaar.ir](https://developers.jouyaar.ir)**.
|
|
31
|
+
|
|
32
|
+
## Two ways to search
|
|
33
|
+
|
|
34
|
+
| Mode | How | When |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `params=` | structured fields (see `client.categories()`) | you already know the fields — deterministic, no LLM, cheapest |
|
|
37
|
+
| `prompt=` | free-text intent | let the API parse a natural-language request |
|
|
38
|
+
|
|
39
|
+
Pass exactly one. `client.categories()` lists every category and its required/optional fields.
|
|
40
|
+
|
|
41
|
+
## Async
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
import asyncio
|
|
45
|
+
from jouyaar import AsyncJooyaar
|
|
46
|
+
|
|
47
|
+
async def main():
|
|
48
|
+
async with AsyncJooyaar() as client:
|
|
49
|
+
res = await client.search(category="lodging",
|
|
50
|
+
params={"city": "تهران", "checkin": "1404-06-10", "checkout": "1404-06-12"})
|
|
51
|
+
print(len(res.quotes), "results")
|
|
52
|
+
|
|
53
|
+
asyncio.run(main())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Errors
|
|
57
|
+
|
|
58
|
+
All failures raise a `JooyaarError` subclass carrying the server `code` and `request_id`:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import time
|
|
62
|
+
from jouyaar import Jooyaar, RateLimitError, QuotaExceededError, AuthenticationError
|
|
63
|
+
|
|
64
|
+
client = Jooyaar()
|
|
65
|
+
try:
|
|
66
|
+
res = client.search(category="flight", prompt="…")
|
|
67
|
+
except RateLimitError as e:
|
|
68
|
+
time.sleep(e.retry_after or 1) # per-minute limit — back off and retry
|
|
69
|
+
except QuotaExceededError:
|
|
70
|
+
... # monthly quota spent — upgrade the plan
|
|
71
|
+
except AuthenticationError:
|
|
72
|
+
... # bad/revoked key
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`RateLimitError` (429, per-minute) and 5xx responses are **retried automatically** with backoff
|
|
76
|
+
(honoring `Retry-After`); 4xx are not. Tune with `Jooyaar(max_retries=..., timeout=...)`.
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
| Argument | Env var | Default |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `api_key` | `JOUYAAR_API_KEY` | — (required) |
|
|
83
|
+
| `base_url` | `JOUYAAR_BASE_URL` | `https://api.jouyaar.ir` |
|
|
84
|
+
| `timeout` | — | `30.0` |
|
|
85
|
+
| `max_retries` | — | `2` |
|
|
86
|
+
|
|
87
|
+
## Development
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv sync --extra dev
|
|
91
|
+
uv run pytest
|
|
92
|
+
uv run ruff check src tests
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
MIT licensed.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Quickstart — set JOUYAAR_API_KEY (and JOUYAAR_BASE_URL for a non-prod host), then run:
|
|
2
|
+
|
|
3
|
+
uv run --with . python examples/quickstart.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from jouyaar import Jooyaar, RateLimitError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main() -> None:
|
|
10
|
+
with Jooyaar() as client: # reads JOUYAAR_API_KEY
|
|
11
|
+
print("Plan/usage:", client.usage())
|
|
12
|
+
|
|
13
|
+
print("\nCategories:")
|
|
14
|
+
for cat in client.categories():
|
|
15
|
+
required = [f.name for f in cat.fields if f.required]
|
|
16
|
+
print(f" - {cat.name} ({cat.fa_title}) required={required}")
|
|
17
|
+
|
|
18
|
+
# Structured (LLM-free) search — cheapest, deterministic.
|
|
19
|
+
try:
|
|
20
|
+
res = client.search(
|
|
21
|
+
category="flight",
|
|
22
|
+
params={"origin": "THR", "destination": "MHD", "departure_date": "1404-06-10", "passengers": 1},
|
|
23
|
+
sort="price",
|
|
24
|
+
)
|
|
25
|
+
except RateLimitError as e:
|
|
26
|
+
print(f"rate limited; retry after {e.retry_after}s")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
print(f"\nSearch ok={res.ok} — {len(res.quotes)} quotes")
|
|
30
|
+
for q in res.quotes[:5]:
|
|
31
|
+
print(f" {q.provider:12} {q.price_toman:>12,} تومان {q.plan_name}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "jouyaar"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Jooyar (جویار) agentic search API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Jooyar" }]
|
|
9
|
+
keywords = ["jooyar", "jouyaar", "search", "api", "sdk", "iran"]
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx>=0.27",
|
|
12
|
+
"pydantic>=2.7",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
dev = [
|
|
17
|
+
"pytest>=8.0",
|
|
18
|
+
"pytest-asyncio>=0.23",
|
|
19
|
+
"respx>=0.21",
|
|
20
|
+
"ruff>=0.9",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://developers.jouyaar.ir"
|
|
25
|
+
Documentation = "https://developers.jouyaar.ir"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/jouyaar"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
asyncio_mode = "auto"
|
|
36
|
+
pythonpath = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.ruff]
|
|
39
|
+
line-length = 100
|
|
40
|
+
target-version = "py39"
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["F", "E", "W", "I", "UP", "B", "SIM", "RUF"]
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint.per-file-ignores]
|
|
46
|
+
# Test fixtures carry long inline JSON literals; readability beats wrapping them.
|
|
47
|
+
"tests/*" = ["E501"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Jooyar (جویار) — official Python SDK for the agentic search API.
|
|
2
|
+
|
|
3
|
+
from jouyaar import Jooyaar
|
|
4
|
+
client = Jooyaar(api_key="sk_live_…") # or set JOUYAAR_API_KEY
|
|
5
|
+
res = client.search(category="flight", prompt="پرواز تهران به مشهد فردا صبح")
|
|
6
|
+
for q in res.quotes:
|
|
7
|
+
print(q.provider, q.price_toman)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
from .client import AsyncJooyaar, Jooyaar
|
|
12
|
+
from .errors import (
|
|
13
|
+
APIConnectionError,
|
|
14
|
+
APIStatusError,
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
InvalidRequestError,
|
|
17
|
+
JooyaarError,
|
|
18
|
+
QuotaExceededError,
|
|
19
|
+
RateLimitError,
|
|
20
|
+
)
|
|
21
|
+
from .models import (
|
|
22
|
+
Category,
|
|
23
|
+
FieldInfo,
|
|
24
|
+
ProviderMeta,
|
|
25
|
+
Quote,
|
|
26
|
+
SearchResponse,
|
|
27
|
+
SearchResult,
|
|
28
|
+
Seller,
|
|
29
|
+
Usage,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"APIConnectionError",
|
|
34
|
+
"APIStatusError",
|
|
35
|
+
"AsyncJooyaar",
|
|
36
|
+
"AuthenticationError",
|
|
37
|
+
"Category",
|
|
38
|
+
"FieldInfo",
|
|
39
|
+
"InvalidRequestError",
|
|
40
|
+
"Jooyaar",
|
|
41
|
+
"JooyaarError",
|
|
42
|
+
"ProviderMeta",
|
|
43
|
+
"QuotaExceededError",
|
|
44
|
+
"Quote",
|
|
45
|
+
"RateLimitError",
|
|
46
|
+
"SearchResponse",
|
|
47
|
+
"SearchResult",
|
|
48
|
+
"Seller",
|
|
49
|
+
"Usage",
|
|
50
|
+
"__version__",
|
|
51
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Shared, transport-agnostic helpers for the sync and async clients.
|
|
2
|
+
|
|
3
|
+
Keeps request construction, error mapping, and retry policy in one place so the two clients only
|
|
4
|
+
differ in how they run the HTTP call.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ._version import __version__
|
|
12
|
+
from .errors import (
|
|
13
|
+
APIStatusError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
JooyaarError,
|
|
17
|
+
QuotaExceededError,
|
|
18
|
+
RateLimitError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://api.jouyaar.ir"
|
|
22
|
+
USER_AGENT = f"jouyaar-python/{__version__}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_headers(api_key: str) -> dict:
|
|
26
|
+
return {
|
|
27
|
+
"Authorization": f"Bearer {api_key}",
|
|
28
|
+
"User-Agent": USER_AGENT,
|
|
29
|
+
"Accept": "application/json",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _error_from_body(status: int, body: Any, headers: httpx.Headers) -> JooyaarError:
|
|
35
|
+
"""Map an API error envelope (``{"error": {...}}``) to the right exception class."""
|
|
36
|
+
err = body.get("error", {}) if isinstance(body, dict) else {}
|
|
37
|
+
code = err.get("code")
|
|
38
|
+
message = err.get("message") or err.get("detail") or f"HTTP {status}"
|
|
39
|
+
request_id = err.get("request_id") or headers.get("x-request-id")
|
|
40
|
+
common = {"status_code": status, "code": code, "request_id": request_id}
|
|
41
|
+
|
|
42
|
+
if status == 401:
|
|
43
|
+
return AuthenticationError(message, **common)
|
|
44
|
+
if status == 429:
|
|
45
|
+
if code == "quota_exceeded":
|
|
46
|
+
return QuotaExceededError(message, **common)
|
|
47
|
+
ra = headers.get("retry-after")
|
|
48
|
+
retry_after = int(ra) if ra and ra.isdigit() else None
|
|
49
|
+
return RateLimitError(message, retry_after=retry_after, **common)
|
|
50
|
+
if status in (400, 422):
|
|
51
|
+
return InvalidRequestError(message, **common)
|
|
52
|
+
return APIStatusError(message, **common)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_response(resp: httpx.Response) -> Any:
|
|
56
|
+
"""Return parsed JSON for a 2xx response, or raise the mapped error."""
|
|
57
|
+
try:
|
|
58
|
+
body = resp.json()
|
|
59
|
+
except Exception: # non-JSON body (proxy error page, etc.)
|
|
60
|
+
body = None
|
|
61
|
+
if resp.is_success:
|
|
62
|
+
return body
|
|
63
|
+
raise _error_from_body(resp.status_code, body, resp.headers)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def should_retry(status: Optional[int]) -> bool:
|
|
67
|
+
"""Retry transient failures: rate limits and 5xx. Never retry 4xx (except 429)."""
|
|
68
|
+
return status is not None and (status == 429 or status >= 500)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def retry_delay(attempt: int, resp: Optional[httpx.Response]) -> float:
|
|
72
|
+
"""Backoff seconds for a retry: honor ``Retry-After`` if present, else exponential."""
|
|
73
|
+
if resp is not None:
|
|
74
|
+
ra = resp.headers.get("retry-after")
|
|
75
|
+
if ra and ra.isdigit():
|
|
76
|
+
return float(int(ra))
|
|
77
|
+
return min(2.0**attempt, 10.0) # 1, 2, 4, … capped at 10s
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_search_body(
|
|
81
|
+
category: str, prompt: Optional[str], params: Optional[dict], sort: Optional[str]
|
|
82
|
+
) -> dict:
|
|
83
|
+
if (prompt is None) == (params is None):
|
|
84
|
+
raise ValueError("Provide exactly one of `prompt` or `params`.")
|
|
85
|
+
body: dict = {"category": category}
|
|
86
|
+
if prompt is not None:
|
|
87
|
+
body["prompt"] = prompt
|
|
88
|
+
if params is not None:
|
|
89
|
+
body["params"] = params
|
|
90
|
+
if sort is not None:
|
|
91
|
+
body["sort"] = sort
|
|
92
|
+
return body
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|