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.
@@ -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 }}
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .env
@@ -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.
@@ -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"