arabam-mcp 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,20 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # required for PyPI trusted publishing (OIDC)
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,61 @@
1
+ name: Publish to MCP Registry
2
+
3
+ # Official MCP Registry (registry.modelcontextprotocol.io) via GitHub OIDC.
4
+ # No secret, no interactive login: the runner's OIDC token proves ownership
5
+ # of the io.github.mohammad-abbasi-sh/* namespace.
6
+ # Refs: https://github.com/modelcontextprotocol/registry
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ id-token: write # required for OIDC -> MCP Registry
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Sync server.json version to tag
23
+ # Only sync when running on a version tag (v*). On workflow_dispatch
24
+ # (ref = refs/heads/main) keep the version committed in server.json.
25
+ run: |
26
+ VER=""
27
+ if [[ "$GITHUB_REF" == refs/tags/* ]]; then
28
+ TAG="${GITHUB_REF#refs/tags/}"
29
+ VER="${TAG#v}"
30
+ fi
31
+ if [ -n "$VER" ]; then
32
+ python3 - "$VER" << 'EOF'
33
+ import json, sys
34
+ ver = sys.argv[1]
35
+ d = json.load(open('server.json'))
36
+ d['version'] = ver
37
+ for pkg in d.get('packages', []):
38
+ pkg['version'] = ver
39
+ json.dump(d, open('server.json', 'w'), indent=2, ensure_ascii=False)
40
+ print('server.json version ->', ver)
41
+ EOF
42
+ else
43
+ echo "No version tag ($GITHUB_REF); keeping server.json version as-is"
44
+ fi
45
+
46
+ - name: Install mcp-publisher
47
+ run: |
48
+ curl -sL "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.7/mcp-publisher_linux_amd64.tar.gz" -o /tmp/mcp-publisher.tar.gz
49
+ tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp
50
+ sudo mv /tmp/mcp-publisher /usr/local/bin/mcp-publisher
51
+ mcp-publisher --version
52
+
53
+ - name: Validate server.json
54
+ run: mcp-publisher validate server.json
55
+
56
+ - name: Publish to MCP Registry (OIDC)
57
+ run: |
58
+ mcp-publisher login github-oidc
59
+ mcp-publisher publish server.json
60
+ # A registry outage must never break the release; re-run from Actions if needed.
61
+ continue-on-error: true
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ *.egg-info/
6
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Abbasi
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,85 @@
1
+ Metadata-Version: 2.5
2
+ Name: arabam-mcp
3
+ Version: 0.1.0
4
+ Summary: Unofficial read-only MCP server for arabam.com used-car listings (search, details, price stats)
5
+ Author: Mohammad Abbasi
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: mcp<2
11
+ Description-Content-Type: text/markdown
12
+
13
+ # arabam-mcp 🇹🇷
14
+
15
+ mcp-name: io.github.mohammad-abbasi-sh/arabam-mcp
16
+
17
+ Unofficial **read-only** MCP server for **arabam.com**, Türkiye's used-car marketplace (~176k listings) — live search, full listing details with the 19-field spec table, brand/model browser, and market price stats.
18
+
19
+ > Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with arabam.com.
20
+
21
+ ## For AI agents
22
+
23
+ Add to your MCP client config (stdio):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "arabam": {
29
+ "command": "uvx",
30
+ "args": ["arabam-mcp"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ Or run locally: `python -m arabam_mcp.server` (needs `mcp<2`, `httpx`).
37
+
38
+ ## Tools
39
+
40
+ | Tool | Description |
41
+ |------|-------------|
42
+ | `arabam_search` | Search listings — `brand` slug ("bmw"), `model` slug ("bmw-3-serisi"), `page` 1..50 |
43
+ | `arabam_listing_detail` | Full detail of one listing (URL from search): price + 19-field spec table |
44
+ | `arabam_list_brands` | All ~77 brands with listing counts and URL slugs |
45
+ | `arabam_list_models` | Models of a brand (e.g. "bmw") with counts and slugs |
46
+ | `arabam_price_stats` | Market stats: min/max/median/average TRY price over N pages |
47
+
48
+ Example output (`arabam_search` / bmw 3-serisi):
49
+
50
+ ```json
51
+ {"brand": "", "model": "bmw-3-serisi", "page": 1, "count": 20,
52
+ "listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
53
+ "brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
54
+ "url": "https://www.arabam.com/ilan/.../45288789"}]}
55
+ ```
56
+
57
+ Example output (`arabam_listing_detail`):
58
+
59
+ ```json
60
+ {"title": "Galeriden Honda Civic 1.4 S 1998 Model …", "price": 425000,
61
+ "price_currency": "TRY", "price_text": "425.000 TL",
62
+ "specs": {"Marka": "Honda", "Seri": "Civic", "Model": "1.4 S",
63
+ "Yıl": "1998", "Kilometre": "330.000 KM", "Vites tipi": "Düz",
64
+ "Yakıt tipi": "LPG & benzin", "Kimden": "Galeriden", …}}
65
+ ```
66
+
67
+ Notes:
68
+ - Data comes from arabam.com's public server-rendered pages (schema.org Vehicle/Car JSON-LD + spec table) — no key, no login, read-only.
69
+ - Spec-table labels are Turkish (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
70
+ - Requests are spaced ~0.8s apart to stay polite.
71
+ - Environments behind an MITM proxy: set `SSL_CERT_FILE` to a CA bundle that trusts the proxy.
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
77
+ .venv/bin/python -m pytest tests/ -q
78
+ ```
79
+
80
+ - `tests/test_listings.py` — offline, parses real captured fixtures
81
+ - `tests/test_live.py` — 5 cautious live requests against arabam.com (needs `SSL_CERT_FILE` behind the lab proxy)
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,61 @@
1
+ # arabam-mcp 🇹🇷
2
+
3
+ سرور MCP غیررسمی و **فقط‌خواندنی** برای **arabam.com**، بازار خودروهای دست‌دوم ترکیه (حدود ۱۷۶ هزار آگهی) — جست‌وجوی زنده، جزئیات کامل هر آگهی با جدول ۱۹ فیلدی مشخصات، مرور برند/مدل و آمار قیمت بازار.
4
+
5
+ > ساختهٔ محمد عباسی در قالب «کارخانهٔ Turkey MCP» (خط تولید به سبک سپهر). وابستگی به arabam.com ندارد.
6
+
7
+ ## برای ایجنت‌های هوش مصنوعی
8
+
9
+ به پیکربندی کلاینت MCP خود (stdio) اضافه کنید:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "arabam": {
15
+ "command": "uvx",
16
+ "args": ["arabam-mcp"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ یا به‌صورت محلی اجرا کنید: `python -m arabam_mcp.server` (نیازمند `mcp<2` و `httpx`).
23
+
24
+ ## ابزارها
25
+
26
+ | ابزار | توضیح |
27
+ |------|-------|
28
+ | `arabam_search` | جست‌وجوی آگهی‌ها — `brand` مثل «bmw»، `model` مثل «bmw-3-serisi»، `page` از ۱ تا ۵۰ |
29
+ | `arabam_listing_detail` | جزئیات کامل یک آگهی (آدرس از خروجی جست‌وجو): قیمت + جدول ۱۹ فیلدی مشخصات |
30
+ | `arabam_list_brands` | همهٔ ~۷۷ برند با تعداد آگهی و نام کوتاه آدرس |
31
+ | `arabam_list_models` | مدل‌های یک برند (مثلاً «bmw») با تعداد و نام کوتاه |
32
+ | `arabam_price_stats` | آمار بازار: کمینه/بیشینه/میانه/میانگین قیمت (لیر) روی N صفحه |
33
+
34
+ خروجی نمونه (`arabam_search` / ب‌ام‌و سری ۳):
35
+
36
+ ```json
37
+ {"model": "bmw-3-serisi", "page": 1, "count": 20,
38
+ "listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
39
+ "brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
40
+ "url": "https://www.arabam.com/ilan/.../45288789"}]}
41
+ ```
42
+
43
+ نکته‌ها:
44
+ - داده‌ها از صفحات عمومی سمت‌سرور arabam.com می‌آیند (JSON-LD از نوع Vehicle/Car در schema.org + جدول مشخصات) — بدون کلید، بدون ورود، فقط خواندنی.
45
+ - برچسب‌های جدول مشخصات ترکی‌اند (Marka، Seri، Model، Yıl، Kilometre، Vites tipi، Yakıt tipi، Kasa tipi، Renk، Motor hacmi، Motor gücü، Çekiş، Kimden…).
46
+ - بین درخواست‌ها حدود ۰٫۸ ثانیه فاصله گذاشته می‌شود تا به سایت فشار نیاید.
47
+ - پشت پروکسی MITM: متغیر `SSL_CERT_FILE` را به بستهٔ CA مورد اعتماد پروکسی بدهید.
48
+
49
+ ## توسعه
50
+
51
+ ```bash
52
+ python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
53
+ .venv/bin/python -m pytest tests/ -q
54
+ ```
55
+
56
+ - `tests/test_listings.py` — آفلاین، با نمونه‌های واقعی ذخیره‌شده
57
+ - `tests/test_live.py` — ۵ درخواست محتاطانهٔ زنده به arabam.com
58
+
59
+ ## مجوز
60
+
61
+ MIT
@@ -0,0 +1,61 @@
1
+ # arabam-mcp 🇹🇷
2
+
3
+ Türkiye'nin ikinci el otomobil pazaryeri **arabam.com** için resmi olmayan, **salt okunur** MCP sunucusu — canlı ilan arama, 19 alanlık özellik tablosuyla tam ilan detayı, marka/model tarama ve piyasa fiyat istatistikleri.
4
+
5
+ > Mohammad Abbasi tarafından "Turkey MCP factory" (Sepehr tarzı hat) kapsamında geliştirildi. arabam.com ile bağlantısı yoktur.
6
+
7
+ ## Yapay zekâ ajanları için
8
+
9
+ MCP istemci yapılandırmanıza (stdio) ekleyin:
10
+
11
+ ```json
12
+ {
13
+ "mcpServers": {
14
+ "arabam": {
15
+ "command": "uvx",
16
+ "args": ["arabam-mcp"]
17
+ }
18
+ }
19
+ }
20
+ ```
21
+
22
+ Veya yerelde çalıştırın: `python -m arabam_mcp.server` (`mcp<2`, `httpx` gerekir).
23
+
24
+ ## Araçlar
25
+
26
+ | Araç | Açıklama |
27
+ |------|----------|
28
+ | `arabam_search` | İlan arama — `brand` ("bmw"), `model` ("bmw-3-serisi"), `page` 1..50 |
29
+ | `arabam_listing_detail` | Tek ilanın tam detayı (arama sonucundaki URL): fiyat + 19 alanlık özellik tablosu |
30
+ | `arabam_list_brands` | ~77 markanın tamamı, ilan sayıları ve URL kısaltmalarıyla |
31
+ | `arabam_list_models` | Bir markanın modelleri (örn. "bmw"), sayılar ve kısaltmalarla |
32
+ | `arabam_price_stats` | Piyasa istatistiği: N sayfadaki ilanların en düşük/en yüksek/ortanca/ortalama fiyatı (TL) |
33
+
34
+ Örnek çıktı (`arabam_search` / bmw 3-serisi):
35
+
36
+ ```json
37
+ {"model": "bmw-3-serisi", "page": 1, "count": 20,
38
+ "listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
39
+ "brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
40
+ "url": "https://www.arabam.com/ilan/.../45288789"}]}
41
+ ```
42
+
43
+ Notlar:
44
+ - Veriler arabam.com'un herkese açık sunucu taraflı sayfalarından gelir (schema.org Vehicle/Car JSON-LD + özellik tablosu) — anahtar yok, giriş yok, salt okunur.
45
+ - Özellik tablosu etiketleri Türkçedir (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
46
+ - İstekler arasında ~0,8 sn beklenir (nazik kullanım).
47
+ - MITM proxy arkasındaki ortamlar: `SSL_CERT_FILE` ile proxy'ye güvenen bir CA paketi gösterin.
48
+
49
+ ## Geliştirme
50
+
51
+ ```bash
52
+ python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
53
+ .venv/bin/python -m pytest tests/ -q
54
+ ```
55
+
56
+ - `tests/test_listings.py` — çevrimdışı, gerçek yakalanmış örneklerle
57
+ - `tests/test_live.py` — arabam.com'a karşı 5 temkinli canlı istek
58
+
59
+ ## Lisans
60
+
61
+ MIT
@@ -0,0 +1,73 @@
1
+ # arabam-mcp 🇹🇷
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/arabam-mcp
4
+
5
+ Unofficial **read-only** MCP server for **arabam.com**, Türkiye's used-car marketplace (~176k listings) — live search, full listing details with the 19-field spec table, brand/model browser, and market price stats.
6
+
7
+ > Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with arabam.com.
8
+
9
+ ## For AI agents
10
+
11
+ Add to your MCP client config (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "arabam": {
17
+ "command": "uvx",
18
+ "args": ["arabam-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Or run locally: `python -m arabam_mcp.server` (needs `mcp<2`, `httpx`).
25
+
26
+ ## Tools
27
+
28
+ | Tool | Description |
29
+ |------|-------------|
30
+ | `arabam_search` | Search listings — `brand` slug ("bmw"), `model` slug ("bmw-3-serisi"), `page` 1..50 |
31
+ | `arabam_listing_detail` | Full detail of one listing (URL from search): price + 19-field spec table |
32
+ | `arabam_list_brands` | All ~77 brands with listing counts and URL slugs |
33
+ | `arabam_list_models` | Models of a brand (e.g. "bmw") with counts and slugs |
34
+ | `arabam_price_stats` | Market stats: min/max/median/average TRY price over N pages |
35
+
36
+ Example output (`arabam_search` / bmw 3-serisi):
37
+
38
+ ```json
39
+ {"brand": "", "model": "bmw-3-serisi", "page": 1, "count": 20,
40
+ "listings": [{"id": "45288789", "title": "214 BİN KMDE SORUNSUZ A4 TDI",
41
+ "brand": "Audi", "year": 2019, "km": 214000, "price_try": 789900,
42
+ "url": "https://www.arabam.com/ilan/.../45288789"}]}
43
+ ```
44
+
45
+ Example output (`arabam_listing_detail`):
46
+
47
+ ```json
48
+ {"title": "Galeriden Honda Civic 1.4 S 1998 Model …", "price": 425000,
49
+ "price_currency": "TRY", "price_text": "425.000 TL",
50
+ "specs": {"Marka": "Honda", "Seri": "Civic", "Model": "1.4 S",
51
+ "Yıl": "1998", "Kilometre": "330.000 KM", "Vites tipi": "Düz",
52
+ "Yakıt tipi": "LPG & benzin", "Kimden": "Galeriden", …}}
53
+ ```
54
+
55
+ Notes:
56
+ - Data comes from arabam.com's public server-rendered pages (schema.org Vehicle/Car JSON-LD + spec table) — no key, no login, read-only.
57
+ - Spec-table labels are Turkish (Marka, Seri, Model, Yıl, Kilometre, Vites tipi, Yakıt tipi, Kasa tipi, Renk, Motor hacmi, Motor gücü, Çekiş, Kimden…).
58
+ - Requests are spaced ~0.8s apart to stay polite.
59
+ - Environments behind an MITM proxy: set `SSL_CERT_FILE` to a CA bundle that trusts the proxy.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ python3 -m venv .venv && .venv/bin/pip install "mcp<2" httpx pytest
65
+ .venv/bin/python -m pytest tests/ -q
66
+ ```
67
+
68
+ - `tests/test_listings.py` — offline, parses real captured fixtures
69
+ - `tests/test_live.py` — 5 cautious live requests against arabam.com (needs `SSL_CERT_FILE` behind the lab proxy)
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "arabam-mcp"
7
+ version = "0.1.0"
8
+ description = "Unofficial read-only MCP server for arabam.com used-car listings (search, details, price stats)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "mcp<2",
14
+ "httpx>=0.27",
15
+ ]
16
+ authors = [{ name = "Mohammad Abbasi" }]
17
+
18
+ [project.scripts]
19
+ arabam-mcp = "arabam_mcp.server:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/arabam_mcp"]
@@ -0,0 +1 @@
1
+ {"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"Unofficial read-only MCP server for arabam.com used-car listings: search, details, price stats.","name":"io.github.mohammad-abbasi-sh/arabam-mcp","packages":[{"identifier":"arabam-mcp","registryBaseUrl":"https://pypi.org","registryType":"pypi","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/mohammad-abbasi-sh/arabam-mcp"},"title":"arabam.com Car Listings","version":"0.1.0","websiteUrl":"https://github.com/mohammad-abbasi-sh/arabam-mcp"}
@@ -0,0 +1,3 @@
1
+ """arabam-mcp: unofficial read-only MCP server for arabam.com car listings."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,117 @@
1
+ """HTTP layer for arabam_mcp — polite scraping of public SSR pages.
2
+
3
+ arabam.com serves fully server-rendered pages with no bot wall: listing data
4
+ comes from embedded JSON-LD (schema.org Vehicle/Car) and a facets JSON blob.
5
+ We keep requests gentle (one at a time, ~0.8s apart) and read-only.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import re
11
+ import time
12
+
13
+ import httpx
14
+
15
+ BASE = "https://www.arabam.com"
16
+ UA = (
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
18
+ "(KHTML, like Gecko) Chrome/120.0 Safari/537.36"
19
+ )
20
+ TIMEOUT = 25.0
21
+ MIN_INTERVAL = 0.8 # seconds between requests — be polite
22
+
23
+ _last_fetch = 0.0
24
+
25
+
26
+ class ArabamError(RuntimeError):
27
+ pass
28
+
29
+
30
+ def _client() -> httpx.Client:
31
+ # NOTE: proxy URL is read from the environment but never logged.
32
+ # trust_env=False because httpx fails to parse bracketed IPv6 entries
33
+ # (e.g. [::1]) that some environments put in no_proxy.
34
+ proxy = os.environ.get("https_proxy") or os.environ.get("HTTPS_PROXY")
35
+ # Standard SSL_CERT_FILE convention lets MITM-proxied environments
36
+ # point at a CA bundle that trusts the proxy; default uses certifi,
37
+ # which is correct for normal users.
38
+ verify: object = True
39
+ cafile = os.environ.get("SSL_CERT_FILE")
40
+ if cafile:
41
+ import ssl
42
+
43
+ verify = ssl.create_default_context(cafile=cafile)
44
+ return httpx.Client(
45
+ headers={"User-Agent": UA, "Accept-Language": "tr-TR,tr;q=0.9"},
46
+ timeout=TIMEOUT,
47
+ proxy=proxy or None,
48
+ verify=verify,
49
+ trust_env=False,
50
+ follow_redirects=True,
51
+ )
52
+
53
+
54
+ def search_url(brand: str | None, model: str | None, page: int) -> str:
55
+ slug_re = re.compile(r"^[a-z0-9][a-z0-9-]*$")
56
+ path = "ikinci-el/otomobil"
57
+ if model:
58
+ m = model.strip().lower()
59
+ if not slug_re.fullmatch(m):
60
+ raise ArabamError(f"bad model slug: {model!r}")
61
+ path += f"/{m}"
62
+ elif brand:
63
+ b = brand.strip().lower()
64
+ if not slug_re.fullmatch(b):
65
+ raise ArabamError(f"bad brand slug: {brand!r}")
66
+ path += f"/{b}"
67
+ url = f"{BASE}/{path}"
68
+ if page and page > 1:
69
+ url += f"?page={int(page)}"
70
+ return url
71
+
72
+
73
+ def fetch(url: str) -> str:
74
+ """GET a page; raises ArabamError on failure or bot-wall."""
75
+ global _last_fetch
76
+ wait = MIN_INTERVAL - (time.monotonic() - _last_fetch)
77
+ if wait > 0:
78
+ time.sleep(wait)
79
+ try:
80
+ with _client() as c:
81
+ r = c.get(url)
82
+ except httpx.HTTPError as e:
83
+ raise ArabamError(f"network error fetching {url}: {e}") from e
84
+ _last_fetch = time.monotonic()
85
+ if r.status_code != 200:
86
+ raise ArabamError(f"arabam.com returned HTTP {r.status_code} for {url}")
87
+ text = r.text
88
+ if 'name="description" content="px-captcha"' in text or "PerimeterX" in text:
89
+ raise ArabamError("arabam.com is showing a bot check for this request")
90
+ return text
91
+
92
+
93
+ def fetch_search(brand: str | None = None, model: str | None = None,
94
+ page: int = 1) -> str:
95
+ if page is not None and (int(page) < 1 or int(page) > 50):
96
+ raise ArabamError("page must be between 1 and 50")
97
+ return fetch(search_url(brand, model, int(page) if page else 1))
98
+
99
+
100
+ def fetch_detail(url: str) -> str:
101
+ u = url.strip()
102
+ if u.startswith("/"):
103
+ u = BASE + u
104
+ if not re.fullmatch(r"https://www\.arabam\.com/ilan/[a-z0-9\-/]+", u):
105
+ raise ArabamError(
106
+ "detail URL must be an arabam.com /ilan/ listing URL "
107
+ "(get one from arabam_search first)"
108
+ )
109
+ return fetch(u)
110
+
111
+
112
+ def fetch_brands_page() -> str:
113
+ return fetch(f"{BASE}/ikinci-el/otomobil")
114
+
115
+
116
+ def fetch_brand_page(brand: str) -> str:
117
+ return fetch(search_url(brand, None, 1))