sok-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.
- sok_mcp-0.1.0/.github/workflows/publish.yml +20 -0
- sok_mcp-0.1.0/.github/workflows/registry.yml +61 -0
- sok_mcp-0.1.0/PKG-INFO +82 -0
- sok_mcp-0.1.0/README-fa.md +40 -0
- sok_mcp-0.1.0/README-tr.md +42 -0
- sok_mcp-0.1.0/README.md +70 -0
- sok_mcp-0.1.0/pyproject.toml +23 -0
- sok_mcp-0.1.0/server.json +1 -0
- sok_mcp-0.1.0/src/sok_mcp/__init__.py +2 -0
- sok_mcp-0.1.0/src/sok_mcp/http.py +93 -0
- sok_mcp-0.1.0/src/sok_mcp/products.py +156 -0
- sok_mcp-0.1.0/src/sok_mcp/server.py +130 -0
- sok_mcp-0.1.0/tests/__init__.py +0 -0
- sok_mcp-0.1.0/tests/fixtures/home.html +23 -0
- sok_mcp-0.1.0/tests/fixtures/kampanyalar.html +4 -0
- sok_mcp-0.1.0/tests/fixtures/product.html +10 -0
- sok_mcp-0.1.0/tests/fixtures/search.html +13 -0
- sok_mcp-0.1.0/tests/test_live.py +64 -0
- sok_mcp-0.1.0/tests/test_products.py +84 -0
|
@@ -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
|
sok_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sok-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial read-only MCP server for ŞOK grocery (sokmarket.com.tr) products, categories, campaigns, prices
|
|
5
|
+
Author: Mohammad Abbasi
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: curl-cffi>=0.7
|
|
9
|
+
Requires-Dist: mcp<2
|
|
10
|
+
Requires-Dist: requests>=2.32
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# sok-mcp 🇹🇷
|
|
14
|
+
|
|
15
|
+
mcp-name: io.github.mohammad-abbasi-sh/sok-mcp
|
|
16
|
+
|
|
17
|
+
Unofficial **read-only** MCP server for **ŞOK** (sokmarket.com.tr) — one of Türkiye's big discount grocery chains: product search, product details, categories, campaign groups and price statistics.
|
|
18
|
+
|
|
19
|
+
> Part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with ŞOK.
|
|
20
|
+
|
|
21
|
+
## For AI agents
|
|
22
|
+
|
|
23
|
+
Add to your MCP client config (stdio):
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"mcpServers": {
|
|
28
|
+
"sok": {
|
|
29
|
+
"command": "uvx",
|
|
30
|
+
"args": ["sok-mcp"]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or run locally: `python -m sok_mcp.server` (needs `mcp<2`, `curl-cffi`, `requests`).
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
| Tool | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `sok_search_products` | Search products by name — `query`, `limit` (1..30, first page only) |
|
|
43
|
+
| `sok_get_product` | Product detail — `slug_or_url` (e.g. `...-p-320230` or a full sokmarket.com.tr URL) |
|
|
44
|
+
| `sok_list_categories` | Store categories (name, slug, id, url) |
|
|
45
|
+
| `sok_category_products` | Products in one category — `category` slug or URL, `limit` (1..30) |
|
|
46
|
+
| `sok_list_promotions` | Current campaign groups from `/kampanyalar` |
|
|
47
|
+
| `sok_price_stats` | Price stats for a search — count, min, max, median, mean in TRY |
|
|
48
|
+
|
|
49
|
+
Example output (`sok_search_products` / "süt"):
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"query": "süt", "total": 207,
|
|
53
|
+
"products": [{"title": "Mis Uht Süt Yarım Yağlı 1 L", "id": "5834",
|
|
54
|
+
"url": "https://www.sokmarket.com.tr/mis-uht-sut-yarim-yagli-1-l-p-5834",
|
|
55
|
+
"price_try": 43.5}]}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Example output (`sok_price_stats` / "süt", first page of 10):
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{"query": "süt", "total_matches": 207, "count": 10,
|
|
62
|
+
"min_try": 16.5, "max_try": 88.5, "median_try": 55.75, "mean_try": 52.05}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Notes:
|
|
66
|
+
- Prices are Turkish Lira (TRY); the site writes them Turkish-style (`43,50₺`).
|
|
67
|
+
- Search/category pages return the first page (~20 products); `total` reports all matches.
|
|
68
|
+
- Data: sokmarket.com.tr public server-rendered pages — no key, no login, read-only.
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" curl-cffi requests pytest
|
|
74
|
+
.venv/bin/python -m pytest tests/ -q
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `tests/test_products.py` — offline, parses real captured fixtures
|
|
78
|
+
- `tests/test_live.py` — 5 cautious live requests against sokmarket.com.tr
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# sok-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
سرور MCP غیررسمی و **فقطخواندنی** برای **ŞOK** (sokmarket.com.tr) — یکی از زنجیرههای بزرگ تخفیفی ترکیه: جستوجوی محصول، جزئیات محصول، دستهها، گروههای کمپین و آمار قیمت.
|
|
4
|
+
|
|
5
|
+
> بخشی از «کارخانهی MCP ترکیه» (پایپلاین به سبک سپهر). هیچ وابستگی به ŞOK ندارد.
|
|
6
|
+
|
|
7
|
+
## برای ایجنتهای هوش مصنوعی
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"sok": {
|
|
13
|
+
"command": "uvx",
|
|
14
|
+
"args": ["sok-mcp"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ابزارها
|
|
21
|
+
|
|
22
|
+
| ابزار | توضیح |
|
|
23
|
+
|------|-------|
|
|
24
|
+
| `sok_search_products` | جستوجوی محصول با نام — `query`، `limit` (۱ تا ۳۰، فقط صفحهی اول) |
|
|
25
|
+
| `sok_get_product` | جزئیات محصول — `slug_or_url` (مثلاً `...-p-320230` یا URL کامل sokmarket.com.tr) |
|
|
26
|
+
| `sok_list_categories` | دستههای فروشگاه (نام، slug، id، url) |
|
|
27
|
+
| `sok_category_products` | محصولات یک دسته — `category` (slug یا URL)، `limit` (۱ تا ۳۰) |
|
|
28
|
+
| `sok_list_promotions` | گروههای کمپین فعلی (`/kampanyalar`) |
|
|
29
|
+
| `sok_price_stats` | آمار قیمت یک جستوجو — تعداد، کمترین، بیشترین، میانه، میانگین (لیر) |
|
|
30
|
+
|
|
31
|
+
مثال (`sok_search_products` / «süt»): ۲۰۷ محصول؛ اولین نتیجه «Mis Uht Süt Yarım Yağlı 1 L» — ۴۳٫۵۰ لیر. مثال (`sok_price_stats` / «süt»): صفحهی اول ۱۰ محصول، کمترین ۱۶٫۵۰ لیر، بیشترین ۸۸٫۵۰ لیر، میانه ۵۵٫۷۵ لیر.
|
|
32
|
+
|
|
33
|
+
نکتهها:
|
|
34
|
+
- قیمتها به لیر ترکیه (TRY) هستند؛ سایت به سبک ترکی مینویسد (`43,50₺`).
|
|
35
|
+
- صفحههای جستوجو/دسته فقط صفحهی اول را برمیگردانند (~۲۰ محصول)؛ `total` همهی تطابقها را گزارش میکند.
|
|
36
|
+
- داده: صفحات عمومی sokmarket.com.tr — بدون لاگین، بدون کلید، فقطخواندنی.
|
|
37
|
+
|
|
38
|
+
## قوانین
|
|
39
|
+
|
|
40
|
+
غیررسمی و فقطخواندنی — بدون لاگین، بدون کلید، بدون نوشتن. تستها: آفلاین با فیکسچر + تست زنده (۱۵/۱۵ سبز).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# sok-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
**ŞOK** (sokmarket.com.tr) için gayriresmî, **salt-okunur** MCP sunucusu — Türkiye'nin büyük indirim market zincirlerinden biri: ürün arama, ürün detayı, kategoriler, kampanya grupları ve fiyat istatistikleri.
|
|
4
|
+
|
|
5
|
+
> "Türkiye MCP fabrikası" parçası (Sepehr tarzı pipeline). ŞOK ile bağlantısı yoktur.
|
|
6
|
+
|
|
7
|
+
## Yapay zekâ ajanları için
|
|
8
|
+
|
|
9
|
+
MCP istemcinize ekleyin (stdio):
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"sok": {
|
|
15
|
+
"command": "uvx",
|
|
16
|
+
"args": ["sok-mcp"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Araçlar
|
|
23
|
+
|
|
24
|
+
| Araç | Açıklama |
|
|
25
|
+
|------|----------|
|
|
26
|
+
| `sok_search_products` | İsme göre ürün ara — `query`, `limit` (1..30, yalnızca ilk sayfa) |
|
|
27
|
+
| `sok_get_product` | Ürün detayı — `slug_or_url` (örn. `...-p-320230` veya tam sokmarket.com.tr URL'si) |
|
|
28
|
+
| `sok_list_categories` | Mağaza kategorileri (ad, slug, id, url) |
|
|
29
|
+
| `sok_category_products` | Bir kategorideki ürünler — `category` slug veya URL, `limit` (1..30) |
|
|
30
|
+
| `sok_list_promotions` | Güncel kampanya grupları (`/kampanyalar`) |
|
|
31
|
+
| `sok_price_stats` | Bir aramanın fiyat istatistikleri — adet, min, max, medyan, ortalama (TRY) |
|
|
32
|
+
|
|
33
|
+
Örnek (`sok_search_products` / "süt"): 207 ürün; ilk sonuç "Mis Uht Süt Yarım Yağlı 1 L" — 43,50₺. Örnek (`sok_price_stats` / "süt"): ilk sayfada 10 ürün, min 16,50₺, max 88,50₺, medyan 55,75₺.
|
|
34
|
+
|
|
35
|
+
Notlar:
|
|
36
|
+
- Fiyatlar Türk Lirası (TRY); site Türkçe yazar (`43,50₺`).
|
|
37
|
+
- Arama/kategori sayfaları ilk sayfayı döner (~20 ürün); `total` tüm eşleşmeleri bildirir.
|
|
38
|
+
- Veri: sokmarket.com.tr herkese açık sayfalar — giriş yok, API anahtarı yok, salt-okunur.
|
|
39
|
+
|
|
40
|
+
## Kurallar
|
|
41
|
+
|
|
42
|
+
Gayriresmî ve salt-okunur — giriş yok, API anahtarı yok, yazma yok. Testler: çevrimdışı fixture + canlı testler (15/15 yeşil).
|
sok_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# sok-mcp 🇹🇷
|
|
2
|
+
|
|
3
|
+
mcp-name: io.github.mohammad-abbasi-sh/sok-mcp
|
|
4
|
+
|
|
5
|
+
Unofficial **read-only** MCP server for **ŞOK** (sokmarket.com.tr) — one of Türkiye's big discount grocery chains: product search, product details, categories, campaign groups and price statistics.
|
|
6
|
+
|
|
7
|
+
> Part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with ŞOK.
|
|
8
|
+
|
|
9
|
+
## For AI agents
|
|
10
|
+
|
|
11
|
+
Add to your MCP client config (stdio):
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"mcpServers": {
|
|
16
|
+
"sok": {
|
|
17
|
+
"command": "uvx",
|
|
18
|
+
"args": ["sok-mcp"]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or run locally: `python -m sok_mcp.server` (needs `mcp<2`, `curl-cffi`, `requests`).
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | Description |
|
|
29
|
+
|------|-------------|
|
|
30
|
+
| `sok_search_products` | Search products by name — `query`, `limit` (1..30, first page only) |
|
|
31
|
+
| `sok_get_product` | Product detail — `slug_or_url` (e.g. `...-p-320230` or a full sokmarket.com.tr URL) |
|
|
32
|
+
| `sok_list_categories` | Store categories (name, slug, id, url) |
|
|
33
|
+
| `sok_category_products` | Products in one category — `category` slug or URL, `limit` (1..30) |
|
|
34
|
+
| `sok_list_promotions` | Current campaign groups from `/kampanyalar` |
|
|
35
|
+
| `sok_price_stats` | Price stats for a search — count, min, max, median, mean in TRY |
|
|
36
|
+
|
|
37
|
+
Example output (`sok_search_products` / "süt"):
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{"query": "süt", "total": 207,
|
|
41
|
+
"products": [{"title": "Mis Uht Süt Yarım Yağlı 1 L", "id": "5834",
|
|
42
|
+
"url": "https://www.sokmarket.com.tr/mis-uht-sut-yarim-yagli-1-l-p-5834",
|
|
43
|
+
"price_try": 43.5}]}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Example output (`sok_price_stats` / "süt", first page of 10):
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{"query": "süt", "total_matches": 207, "count": 10,
|
|
50
|
+
"min_try": 16.5, "max_try": 88.5, "median_try": 55.75, "mean_try": 52.05}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Prices are Turkish Lira (TRY); the site writes them Turkish-style (`43,50₺`).
|
|
55
|
+
- Search/category pages return the first page (~20 products); `total` reports all matches.
|
|
56
|
+
- Data: sokmarket.com.tr public server-rendered pages — no key, no login, read-only.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
python3 -m venv .venv && .venv/bin/pip install "mcp<2" curl-cffi requests pytest
|
|
62
|
+
.venv/bin/python -m pytest tests/ -q
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `tests/test_products.py` — offline, parses real captured fixtures
|
|
66
|
+
- `tests/test_live.py` — 5 cautious live requests against sokmarket.com.tr
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sok-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unofficial read-only MCP server for ŞOK grocery (sokmarket.com.tr) products, categories, campaigns, prices"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp<2",
|
|
14
|
+
"curl-cffi>=0.7",
|
|
15
|
+
"requests>=2.32",
|
|
16
|
+
]
|
|
17
|
+
authors = [{ name = "Mohammad Abbasi" }]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
sok-mcp = "sok_mcp.server:main"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/sok_mcp"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"Unofficial read-only MCP server for ŞOK grocery — products, categories, campaigns, prices.","name":"io.github.mohammad-abbasi-sh/sok-mcp","packages":[{"identifier":"sok-mcp","registryBaseUrl":"https://pypi.org","registryType":"pypi","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/mohammad-abbasi-sh/sok-mcp"},"title":"ŞOK Grocery","version":"0.1.0","websiteUrl":"https://github.com/mohammad-abbasi-sh/sok-mcp"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""HTTP layer for sok_mcp.
|
|
2
|
+
|
|
3
|
+
sokmarket.com.tr is a Next.js storefront that serves its search,
|
|
4
|
+
category and product pages fully server-rendered with no login and
|
|
5
|
+
no API key. Transport uses curl-cffi chrome120 impersonation (the
|
|
6
|
+
proven Turkey-factory pattern) with a plain-requests fallback.
|
|
7
|
+
Requests are kept gentle (one at a time, ~0.8s apart) and read-only.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from urllib.parse import quote
|
|
13
|
+
|
|
14
|
+
from curl_cffi import requests as _creq
|
|
15
|
+
import requests as _plain
|
|
16
|
+
|
|
17
|
+
BASE = "https://www.sokmarket.com.tr"
|
|
18
|
+
TIMEOUT = 30.0
|
|
19
|
+
MIN_INTERVAL = 0.8 # seconds between requests — be polite
|
|
20
|
+
|
|
21
|
+
_HEADERS = {
|
|
22
|
+
"Accept-Language": "tr-TR,tr;q=0.9,en;q=0.8",
|
|
23
|
+
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_UA = (
|
|
27
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
28
|
+
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_last_fetch = 0.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SokError(RuntimeError):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _throttle() -> None:
|
|
39
|
+
global _last_fetch
|
|
40
|
+
wait = MIN_INTERVAL - (time.monotonic() - _last_fetch)
|
|
41
|
+
if wait > 0:
|
|
42
|
+
time.sleep(wait)
|
|
43
|
+
_last_fetch = time.monotonic()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get(url: str) -> str:
|
|
47
|
+
_throttle()
|
|
48
|
+
try:
|
|
49
|
+
resp = _creq.get(url, impersonate="chrome120", headers=_HEADERS, timeout=TIMEOUT)
|
|
50
|
+
status, text = resp.status_code, resp.text
|
|
51
|
+
except Exception:
|
|
52
|
+
# Fallback transport: plain requests with a browser UA.
|
|
53
|
+
try:
|
|
54
|
+
resp = _plain.get(url, headers={**_HEADERS, "User-Agent": _UA}, timeout=TIMEOUT)
|
|
55
|
+
status, text = resp.status_code, resp.text
|
|
56
|
+
except Exception as e:
|
|
57
|
+
raise SokError(f"network error fetching {url}: {e}") from e
|
|
58
|
+
if status == 403:
|
|
59
|
+
raise SokError(
|
|
60
|
+
"sokmarket.com.tr refused the request (HTTP 403, bot wall) — try again later"
|
|
61
|
+
)
|
|
62
|
+
if status == 429:
|
|
63
|
+
raise SokError(
|
|
64
|
+
"sokmarket.com.tr rate-limited the request (HTTP 429) — try again later"
|
|
65
|
+
)
|
|
66
|
+
if status == 404:
|
|
67
|
+
raise SokError(f"sokmarket.com.tr has no page at {url} (HTTP 404)")
|
|
68
|
+
if status != 200:
|
|
69
|
+
raise SokError(f"sokmarket.com.tr returned HTTP {status} for {url}")
|
|
70
|
+
return text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def fetch_search(query: str) -> str:
|
|
74
|
+
return _get(f"{BASE}/arama?q={quote(query)}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def fetch_product(path: str) -> str:
|
|
78
|
+
path = path.strip()
|
|
79
|
+
if path.startswith("http"):
|
|
80
|
+
return _get(path)
|
|
81
|
+
return _get(f"{BASE}/{path.lstrip('/')}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def fetch_home() -> str:
|
|
85
|
+
return _get(f"{BASE}/")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def fetch_category(path: str) -> str:
|
|
89
|
+
return _get(f"{BASE}/{path.strip().lstrip('/')}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def fetch_campaigns() -> str:
|
|
93
|
+
return _get(f"{BASE}/kampanyalar")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Domain logic: parse ŞOK (sokmarket.com.tr) pages into compact dicts.
|
|
2
|
+
|
|
3
|
+
Data sources (all public, no login, no key):
|
|
4
|
+
- search: /arama?q=<query> — server-rendered product cards
|
|
5
|
+
("207 adet ürün listelendi"), title + price per card
|
|
6
|
+
- product: /<slug>-p-<id> — h1 title, price, related products
|
|
7
|
+
- category: /<slug>-c-<id> — same product-card grid as search
|
|
8
|
+
- home: / — 19 categories (-c- links) + promo groups (-pgrp-)
|
|
9
|
+
- promos: /kampanyalar — campaign groups (-pgrp- links)
|
|
10
|
+
Prices are Turkish format ("43,50₺", thousands "1.234,56₺").
|
|
11
|
+
Stdlib parsing only (re / html).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import html as _html
|
|
16
|
+
import re
|
|
17
|
+
import statistics
|
|
18
|
+
|
|
19
|
+
BASE = "https://www.sokmarket.com.tr"
|
|
20
|
+
|
|
21
|
+
_CARD_TITLE = "CProductCard-module_title__u8bMW"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _clean(text: str | None) -> str:
|
|
25
|
+
if not text:
|
|
26
|
+
return ""
|
|
27
|
+
return re.sub(r"\s+", " ", _html.unescape(str(text))).strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_price(text: str | None) -> float | None:
|
|
31
|
+
"""'43,50' / '1.234,56' (Turkish) -> 43.5 / 1234.56."""
|
|
32
|
+
if not text:
|
|
33
|
+
return None
|
|
34
|
+
s = text.strip().replace("₺", "").replace("\xa0", "").strip()
|
|
35
|
+
s = s.replace(".", "").replace(",", ".")
|
|
36
|
+
try:
|
|
37
|
+
return float(s)
|
|
38
|
+
except ValueError:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _price_in(block: str) -> float | None:
|
|
43
|
+
m = re.search(
|
|
44
|
+
r'data-testid="(?:discountedPrice|price)">([\d.,]+)<!-- -->\s*₺', block
|
|
45
|
+
)
|
|
46
|
+
return parse_price(m.group(1)) if m else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_product_cards(html: str) -> dict:
|
|
50
|
+
"""Product-card grid -> {total, products[{title, slug, id, url, price_try}]}.
|
|
51
|
+
|
|
52
|
+
total comes from the "N adet ürün listelendi" line when present.
|
|
53
|
+
"""
|
|
54
|
+
total = None
|
|
55
|
+
m = re.search(r"(\d+)(?:<!-- -->)?\s*adet ürün", html)
|
|
56
|
+
if m:
|
|
57
|
+
total = int(m.group(1))
|
|
58
|
+
products, seen = [], set()
|
|
59
|
+
# Each card is an <a href="/<slug>-p-<id>"> wrapping title + price.
|
|
60
|
+
for link in re.finditer(r'<a href="(/[^"]+-p-(\d+))">', html):
|
|
61
|
+
href, pid = link.group(1), link.group(2)
|
|
62
|
+
if pid in seen:
|
|
63
|
+
continue
|
|
64
|
+
block = html[link.start(): link.start() + 4000]
|
|
65
|
+
t = re.search(
|
|
66
|
+
rf'<h2 class="{re.escape(_CARD_TITLE)}">(.*?)</h2>', block, re.S
|
|
67
|
+
)
|
|
68
|
+
if not t:
|
|
69
|
+
continue
|
|
70
|
+
seen.add(pid)
|
|
71
|
+
products.append(
|
|
72
|
+
{
|
|
73
|
+
"title": _clean(t.group(1)),
|
|
74
|
+
"slug": href.strip("/").rsplit("-p-", 1)[0],
|
|
75
|
+
"id": pid,
|
|
76
|
+
"url": f"{BASE}{href}",
|
|
77
|
+
"price_try": _price_in(block),
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
return {"total": total if total is not None else len(products), "products": products}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def parse_product_page(html: str) -> dict:
|
|
84
|
+
"""Product detail page: h1 title, price, related products."""
|
|
85
|
+
h1 = re.search(r"<h1[^>]*>(.*?)</h1>", html, re.S)
|
|
86
|
+
title = _clean(re.sub(r"<[^>]+>", "", h1.group(1))) if h1 else ""
|
|
87
|
+
if not title:
|
|
88
|
+
raise ValueError("no product title found on this page")
|
|
89
|
+
related = [
|
|
90
|
+
_clean(t)
|
|
91
|
+
for t in re.findall(
|
|
92
|
+
rf'<h2 class="{re.escape(_CARD_TITLE)}">(.*?)</h2>', html, re.S
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
return {
|
|
96
|
+
"title": title,
|
|
97
|
+
"price_try": _price_in(html),
|
|
98
|
+
"related_products": related,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_categories(html: str) -> list[dict]:
|
|
103
|
+
"""Homepage category links: /<slug>-c-<id> with Turkish link text."""
|
|
104
|
+
out, seen = [], set()
|
|
105
|
+
for href, _cid, inner in re.findall(
|
|
106
|
+
r'<a[^>]+href="(/[^"]+-c-(\d+))"[^>]*>(.*?)</a>', html, re.S
|
|
107
|
+
):
|
|
108
|
+
name = _clean(re.sub(r"<[^>]+>", "", inner))
|
|
109
|
+
if not name or href in seen:
|
|
110
|
+
continue
|
|
111
|
+
seen.add(href)
|
|
112
|
+
out.append(
|
|
113
|
+
{
|
|
114
|
+
"name": name,
|
|
115
|
+
"slug": href.strip("/").rsplit("-c-", 1)[0],
|
|
116
|
+
"id": href.rsplit("-c-", 1)[-1],
|
|
117
|
+
"url": f"{BASE}{href}",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def parse_promotions(html: str) -> list[dict]:
|
|
124
|
+
"""Campaign groups: /<slug>-pgrp-<uuid> links with link text."""
|
|
125
|
+
out, seen = [], set()
|
|
126
|
+
for href, inner in re.findall(
|
|
127
|
+
r'<a[^>]+href="(/[^"]+-pgrp-[^"]+)"[^>]*>(.*?)</a>', html, re.S
|
|
128
|
+
):
|
|
129
|
+
name = _clean(re.sub(r"<[^>]+>", "", inner))
|
|
130
|
+
if not name or href in seen:
|
|
131
|
+
continue
|
|
132
|
+
seen.add(href)
|
|
133
|
+
out.append({"name": name, "slug": href.strip("/").rsplit("-pgrp-", 1)[0], "url": f"{BASE}{href}"})
|
|
134
|
+
# The /kampanyalar page also links promos without inner text in some
|
|
135
|
+
# spots; fall back to bare hrefs with a slug-derived name.
|
|
136
|
+
if not out:
|
|
137
|
+
for href in sorted(set(re.findall(r'href="(/[^"]+-pgrp-[^"]+)"', html))):
|
|
138
|
+
slug = href.strip("/").rsplit("-pgrp-", 1)[0]
|
|
139
|
+
out.append(
|
|
140
|
+
{"name": slug.replace("-", " "), "slug": slug, "url": f"{BASE}{href}"}
|
|
141
|
+
)
|
|
142
|
+
return out
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def price_stats(products: list[dict]) -> dict:
|
|
146
|
+
"""min / max / median / mean over products that have a price."""
|
|
147
|
+
prices = sorted(p["price_try"] for p in products if p.get("price_try") is not None)
|
|
148
|
+
if not prices:
|
|
149
|
+
return {"count": 0, "min_try": None, "max_try": None, "median_try": None, "mean_try": None}
|
|
150
|
+
return {
|
|
151
|
+
"count": len(prices),
|
|
152
|
+
"min_try": prices[0],
|
|
153
|
+
"max_try": prices[-1],
|
|
154
|
+
"median_try": statistics.median(prices),
|
|
155
|
+
"mean_try": round(statistics.fmean(prices), 2),
|
|
156
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""MCP server: unofficial read-only access to ŞOK grocery (sokmarket.com.tr)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from . import http as _http
|
|
9
|
+
from . import products as _P
|
|
10
|
+
|
|
11
|
+
mcp = FastMCP("sok-mcp")
|
|
12
|
+
|
|
13
|
+
INSTRUCTIONS = """\
|
|
14
|
+
Unofficial read-only MCP server for ŞOK (sokmarket.com.tr), one of \
|
|
15
|
+
Türkiye's big discount grocery chains — product search, product \
|
|
16
|
+
details, categories, campaign groups and price statistics. All \
|
|
17
|
+
pages are server-rendered Next.js with no login and no API key; \
|
|
18
|
+
prices are in Turkish Lira (TRY, Turkish format "43,50₺"). \
|
|
19
|
+
Search/category pages list the first page of results (~20 \
|
|
20
|
+
products); the `total` field reports how many products matched \
|
|
21
|
+
overall. Read-only; transport uses Chrome TLS impersonation with \
|
|
22
|
+
a plain-requests fallback; requests are spaced ~0.8s apart. Built \
|
|
23
|
+
by Mohammad Abbasi (mohammad-abbasi-sh) — Turkey MCP factory.\
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _err(e: Exception) -> str:
|
|
28
|
+
return json.dumps({"error": str(e)}, ensure_ascii=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _validate_limit(limit: int, lo: int = 1, hi: int = 30) -> int:
|
|
32
|
+
if not lo <= limit <= hi:
|
|
33
|
+
raise ValueError(f"limit must be between {lo} and {hi}")
|
|
34
|
+
return limit
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@mcp.tool()
|
|
38
|
+
def sok_search_products(query: str, limit: int = 20) -> str:
|
|
39
|
+
"""Search ŞOK products by name.
|
|
40
|
+
query: e.g. "süt", "çay", "peynir". limit 1..30 (first page only;
|
|
41
|
+
`total` reports all matches). Returns title, slug, id, url and
|
|
42
|
+
price in TRY per product."""
|
|
43
|
+
try:
|
|
44
|
+
if not query or not query.strip():
|
|
45
|
+
raise ValueError("query must not be empty")
|
|
46
|
+
_validate_limit(limit)
|
|
47
|
+
res = _P.parse_product_cards(_http.fetch_search(query.strip()))
|
|
48
|
+
except (ValueError, _http.SokError) as e:
|
|
49
|
+
return _err(e)
|
|
50
|
+
res["products"] = res["products"][:limit]
|
|
51
|
+
return json.dumps({"query": query.strip(), **res}, ensure_ascii=False)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
def sok_get_product(slug_or_url: str) -> str:
|
|
56
|
+
"""Product detail: title, price in TRY, related products.
|
|
57
|
+
slug_or_url: e.g. "ahmad-tea-classic-earl-grey-demlik-poset-40-li-p-320230"
|
|
58
|
+
or a full sokmarket.com.tr product URL."""
|
|
59
|
+
try:
|
|
60
|
+
s = slug_or_url.strip()
|
|
61
|
+
if not s:
|
|
62
|
+
raise ValueError("slug_or_url must not be empty")
|
|
63
|
+
page = _P.parse_product_page(_http.fetch_product(s))
|
|
64
|
+
except (ValueError, _http.SokError) as e:
|
|
65
|
+
return _err(e)
|
|
66
|
+
return json.dumps({"url": slug_or_url.strip(), **page}, ensure_ascii=False)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@mcp.tool()
|
|
70
|
+
def sok_list_categories() -> str:
|
|
71
|
+
"""ŞOK store categories (name, slug, id, url) from the homepage."""
|
|
72
|
+
try:
|
|
73
|
+
cats = _P.parse_categories(_http.fetch_home())
|
|
74
|
+
except (ValueError, _http.SokError) as e:
|
|
75
|
+
return _err(e)
|
|
76
|
+
return json.dumps({"count": len(cats), "categories": cats}, ensure_ascii=False)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@mcp.tool()
|
|
80
|
+
def sok_category_products(category: str, limit: int = 20) -> str:
|
|
81
|
+
"""Products in one ŞOK category.
|
|
82
|
+
category: slug like "sut-ve-sut-urunleri-c-460" or a full
|
|
83
|
+
sokmarket.com.tr category URL. limit 1..30 (first page only)."""
|
|
84
|
+
try:
|
|
85
|
+
if not category or not category.strip():
|
|
86
|
+
raise ValueError("category must not be empty")
|
|
87
|
+
_validate_limit(limit)
|
|
88
|
+
res = _P.parse_product_cards(_http.fetch_category(category.strip()))
|
|
89
|
+
except (ValueError, _http.SokError) as e:
|
|
90
|
+
return _err(e)
|
|
91
|
+
res["products"] = res["products"][:limit]
|
|
92
|
+
return json.dumps({"category": category.strip(), **res}, ensure_ascii=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
def sok_list_promotions() -> str:
|
|
97
|
+
"""Current ŞOK campaign groups from /kampanyalar
|
|
98
|
+
(name, slug, url — each links to its discounted products)."""
|
|
99
|
+
try:
|
|
100
|
+
promos = _P.parse_promotions(_http.fetch_campaigns())
|
|
101
|
+
except (ValueError, _http.SokError) as e:
|
|
102
|
+
return _err(e)
|
|
103
|
+
return json.dumps({"count": len(promos), "promotions": promos}, ensure_ascii=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@mcp.tool()
|
|
107
|
+
def sok_price_stats(query: str) -> str:
|
|
108
|
+
"""Price statistics for a search: count, min, max, median, mean in
|
|
109
|
+
TRY over the first results page. query: e.g. "süt"."""
|
|
110
|
+
try:
|
|
111
|
+
if not query or not query.strip():
|
|
112
|
+
raise ValueError("query must not be empty")
|
|
113
|
+
res = _P.parse_product_cards(_http.fetch_search(query.strip()))
|
|
114
|
+
stats = _P.price_stats(res["products"])
|
|
115
|
+
except (ValueError, _http.SokError) as e:
|
|
116
|
+
return _err(e)
|
|
117
|
+
return json.dumps(
|
|
118
|
+
{"query": query.strip(), "total_matches": res["total"], **stats},
|
|
119
|
+
ensure_ascii=False,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
mcp.run()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
if __name__ == "__main__":
|
|
128
|
+
import sys
|
|
129
|
+
|
|
130
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<html><body>
|
|
2
|
+
<a href="/yemeklik-malzemeler-c-1770">Yemeklik Malzemeler</a>
|
|
3
|
+
<a href="/et-ve-tavuk-ve-sarkuteri-c-160">Et & Tavuk & Şarküteri</a>
|
|
4
|
+
<a href="/meyve-ve-sebze-c-20">Meyve & Sebze</a>
|
|
5
|
+
<a href="/sut-ve-sut-urunleri-c-460">Süt & Süt Ürünleri</a>
|
|
6
|
+
<a href="/kahvaltilik-c-890">Kahvaltılık</a>
|
|
7
|
+
<a href="/atistirmaliklar-c-20376">Atıştırmalık</a>
|
|
8
|
+
<a href="/icecek-c-20505">İçecek</a>
|
|
9
|
+
<a href="/ekmek-ve-pastane-c-1250">Ekmek & Pastane</a>
|
|
10
|
+
<a href="/dondurulmus-urunler-c-1550">Dondurulmuş Ürünler</a>
|
|
11
|
+
<a href="/dondurma-c-31102">Dondurma</a>
|
|
12
|
+
<a href="/temizlik-c-20647">Temizlik</a>
|
|
13
|
+
<a href="/kagit-urunler-c-20875">Kağıt Ürünleri</a>
|
|
14
|
+
<a href="/kisisel-bakim-ve-kozmetik-c-20395">Kişisel Bakım & Kozmetik</a>
|
|
15
|
+
<a href="/anne-bebek-ve-cocuk-c-20634">Anne - Bebek & Çocuk</a>
|
|
16
|
+
<a href="/oyuncak-c-20644">Oyuncak</a>
|
|
17
|
+
<a href="/ev-ve-yasam-c-20898">Ev & Yaşam</a>
|
|
18
|
+
<a href="/evcil-dostlar-c-20880">Evcil Dostlar</a>
|
|
19
|
+
<a href="/giyim-ve-ayakkabi-ve-aksesuar-c-20886">Giyim & Aksesuar</a>
|
|
20
|
+
<a href="/elektronik-c-22769">Elektronik</a>
|
|
21
|
+
<a href="/win-kazandiran-urunler-pgrp-f353cf31-f728-425e-a453-5774219a76b8">Kazandırır Alışverişin</a>
|
|
22
|
+
<a href="/50-tl-ve-uzeri-indirimli-urunler-pgrp-11d42a6b-df28-4fe6-b1a3-7ad6b8d7f9a0">Kasa Arkası Ürünler</a>
|
|
23
|
+
</body></html>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<html><head><title>Kampanyalar</title></head><body>
|
|
2
|
+
<a href="/win-kazandiran-urunler-pgrp-f353cf31-f728-425e-a453-5774219a76b8">Kazandırır Alışverişin</a>
|
|
3
|
+
<a href="/50-tl-ve-uzeri-indirimli-urunler-pgrp-11d42a6b-df28-4fe6-b1a3-7ad6b8d7f9a0">Kasa Arkası Ürünler</a>
|
|
4
|
+
</body></html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<html><head><title>Ahmad Tea Classic Earl Grey Demlik Poşet 40'lı - Cepte Şok</title></head><body>
|
|
2
|
+
<h1 class="ProductMainInfoArea_productName__bKRVD">Ahmad Tea Classic Earl Grey Demlik Poşet 40'lı</h1>
|
|
3
|
+
<span style="font-size:22px;line-height:20px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">149,00<!-- -->₺</span>
|
|
4
|
+
|
|
5
|
+
<h2>Deren Premium Çay 1000 g</h2>
|
|
6
|
+
<h2>Deren Harman Çay 1000 g</h2>
|
|
7
|
+
<h2>Deren Rize Çay 1000 g</h2>
|
|
8
|
+
<h2>Deren Filiz Çay 500 g</h2>
|
|
9
|
+
<h2>Deren Bergamot Aromalı Çay 500 g</h2>
|
|
10
|
+
</body></html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<html><head><title>Cepte Şok</title></head><body>
|
|
2
|
+
<p>207<!-- --> adet ürün listelendi</p>
|
|
3
|
+
<a href="/mis-uht-sut-yarim-yagli-1-l-p-5834"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/20b8a4b2-4a67-4bb0-a863-e55df8db3631.jpg" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Uht Süt Yarım Yağlı 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">43,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
4
|
+
<a href="/icim-organik-sut-1-l-p-7796"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/642caf4f-b52d-493f-9813-f5d533f778e5.png" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">İçim Organik Süt 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">88,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
5
|
+
<a href="/mis-uht-sut-yagsiz-1-l-p-5179"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/c87d0ff9-d53f-4956-8def-4099f523abea.jpg" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Uht Süt Yağsız 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">36,00<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
6
|
+
<a href="/mis-uht-sut-3-1-yagli-1-l-p-8533"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/691ac110-fa10-46fe-a58a-9126d2c09c64.jpg" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Uht Süt %3.1 Yağlı 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">52,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
7
|
+
<a href="/mis-bakraclik-sut-tam-yagli-1-l-p-7501"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/ddb7fb6c-e722-4e47-a34b-c925ec43a74f.png" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Bakraçlık Süt Tam Yağlı 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">64,00<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
8
|
+
<a href="/mis-pastorize-dogal-icme-sutu-1-l-p-6044"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/e32a6c94-6dd4-487a-a29b-de954e04c9ed.png" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Pastörize Doğal İçme Sütü 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">59,00<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
9
|
+
<a href="/sutas-laktozsuz-sut-200-ml-p-635179"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/328bd4bd-f60b-4526-862e-9bb0599a8ece.png" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Sütaş Laktozsuz Süt 200 ml</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">18,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
10
|
+
<a href="/mis-uht-sut-laktozsuz-1-l-p-5764"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/d2400ad5-2c0a-4f50-af14-e3b763a582f0.jpg" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Uht Süt Laktozsuz 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">62,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
11
|
+
<a href="/icim-rahat-laktozsuz-sut-1-l-p-8430"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/4124dddf-9975-4384-aa45-0f5bce002055.png" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">İçim Rahat Laktozsuz Süt 1 L</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_priceContainer__ZROpc" style="height:26px"><span style="font-size:14px;line-height:16px;font-weight:800" class="CPriceBox-module_price__bYk-c" data-testid="discountedPrice">79,50<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
12
|
+
<a href="/mis-kakaolu-sut-200-ml-p-7284"><div class="CProductCard-module_productCardWrapper__okAmT CProductCard-module_PLPCard__rB4tw"><div class="CQuantityChangerCapsule-module_buttonWrapper__Bed1d CQuantityChangerCapsule-module_singleButtonWrapper__oGhYN CQuantityChangerCapsule-module_small__2YPFl" style="position:absolute;right:8px;top:8px"><button class="CQuantityChangerCapsule-module_plusButton__ABlui CQuantityChangerCapsule-module_small__2YPFl" type="button" style="background-color:#e6eef6;border:1px solid #cddcec"><svg width="14" height="14" viewBox="0 0 12 12" fill="#0551a0" xmlns="http://www.w3.org/2000/svg"><path d="M10.9286 4.92857H7.28571C7.16738 4.92857 7.07143 4.83262 7.07143 4.71429V1.07143C7.07143 0.479736 6.59169 0 6 0C5.40831 0 4.92857 0.479736 4.92857 1.07143V4.71429C4.92857 4.83262 4.83262 4.92857 4.71429 4.92857H1.07143C0.479736 4.92857 0 5.40831 0 6C0 6.59169 0.479736 7.07143 1.07143 7.07143H4.71429C4.83262 7.07143 4.92857 7.16738 4.92857 7.28571V10.9286C4.92857 11.5203 5.40831 12 6 12C6.59169 12 7.07143 11.5203 7.07143 10.9286V7.28571C7.07143 7.16738 7.16738 7.07143 7.28571 7.07143H10.9286C11.5203 7.07143 12 6.59169 12 6C12 5.40831 11.5203 4.92857 10.9286 4.92857Z" fill="#0551a0"></path></svg></button></div><div class="CProductCard-module_productCardContainer__mwUl7"><div class="CProductCard-module_containerTop__35Ma3"><div class="CProductCard-module_imageContainer__aTMdz"><img src="https://images.ceptesok.com/cdn-cgi/image/width=216,height=216,fit=pad,quality=80,format=webp/product-assets/sub-folder/964c914f-6190-4eaa-b8a7-dd0dd9c4e92f.jpg" alt="product-thumb" style="width:108px;height:108px"/></div><div class="CProductCard-module_infoContainer__F8uxY"><h2 class="CProductCard-module_title__u8bMW">Mis Kakaolu Süt 200 ml</h2><div class="CPriceBox-module_cPriceBox__1OWBR CPriceBox-module_productCardPrice__yjBVO"><div class="CPriceBox-module_discountedPriceContainer__nsaTN"><div class="CPriceBox-module_price__bYk-c"><span style="font-size:12px;line-height:16px" data-testid="price">16,50<!-- -->₺</span></div><span style="font-size:18px;line-height:16px" class="CPriceBox-module_discountedPrice__15Ffw" data-testid="discountedPrice">15,00<!-- -->₺</span></div></div></div></div></div></div></a></div></div>
|
|
13
|
+
</body></html>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Live tests — hit the real sokmarket.com.tr. Cautious: a few requests, then stop.
|
|
2
|
+
|
|
3
|
+
Run: pytest tests/test_live.py -v
|
|
4
|
+
Skip: pytest -m "not live" (or set SOK_SKIP_LIVE=1)
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
import unittest
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
|
|
15
|
+
|
|
16
|
+
from sok_mcp import http as _http
|
|
17
|
+
from sok_mcp import products as _P
|
|
18
|
+
|
|
19
|
+
pytestmark = pytest.mark.live
|
|
20
|
+
SKIP = os.environ.get("SOK_SKIP_LIVE") == "1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.skipif(SKIP, reason="SOK_SKIP_LIVE=1")
|
|
24
|
+
class TestLive(unittest.TestCase):
|
|
25
|
+
def test_search_is_real(self):
|
|
26
|
+
res = _P.parse_product_cards(_http.fetch_search("süt"))
|
|
27
|
+
self.assertGreaterEqual(res["total"], 50)
|
|
28
|
+
self.assertGreaterEqual(len(res["products"]), 10)
|
|
29
|
+
self.assertTrue(all(p["price_try"] for p in res["products"]))
|
|
30
|
+
print(f"\nLIVE search 'süt': {res['total']} products, "
|
|
31
|
+
f"first = {res['products'][0]['title']} @ {res['products'][0]['price_try']}₺")
|
|
32
|
+
time.sleep(1)
|
|
33
|
+
|
|
34
|
+
def test_product_is_real(self):
|
|
35
|
+
res = _P.parse_product_cards(_http.fetch_search("süt"))
|
|
36
|
+
first = res["products"][0]
|
|
37
|
+
page = _P.parse_product_page(_http.fetch_product(first["url"]))
|
|
38
|
+
self.assertEqual(page["title"], first["title"])
|
|
39
|
+
self.assertEqual(page["price_try"], first["price_try"])
|
|
40
|
+
print(f"\nLIVE product: {page['title']} @ {page['price_try']}₺, "
|
|
41
|
+
f"{len(page['related_products'])} related")
|
|
42
|
+
time.sleep(1)
|
|
43
|
+
|
|
44
|
+
def test_home_categories_are_real(self):
|
|
45
|
+
cats = _P.parse_categories(_http.fetch_home())
|
|
46
|
+
self.assertGreaterEqual(len(cats), 15)
|
|
47
|
+
print(f"\nLIVE home: {len(cats)} categories, first = {cats[0]['name']}")
|
|
48
|
+
time.sleep(1)
|
|
49
|
+
|
|
50
|
+
def test_campaigns_are_real(self):
|
|
51
|
+
promos = _P.parse_promotions(_http.fetch_campaigns())
|
|
52
|
+
self.assertGreaterEqual(len(promos), 1)
|
|
53
|
+
print(f"\nLIVE campaigns: {len(promos)} — {[p['name'] for p in promos]}")
|
|
54
|
+
time.sleep(1)
|
|
55
|
+
|
|
56
|
+
def test_category_page_is_real(self):
|
|
57
|
+
res = _P.parse_product_cards(_http.fetch_category("sut-ve-sut-urunleri-c-460"))
|
|
58
|
+
self.assertGreaterEqual(len(res["products"]), 10)
|
|
59
|
+
print(f"\nLIVE category süt: {len(res['products'])} products on first page")
|
|
60
|
+
time.sleep(1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
unittest.main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Offline tests — parse real captured ŞOK fixtures, no network."""
|
|
2
|
+
import pathlib
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
|
|
7
|
+
|
|
8
|
+
from sok_mcp import products as P
|
|
9
|
+
|
|
10
|
+
FIX = pathlib.Path(__file__).resolve().parent / "fixtures"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSearchCards(unittest.TestCase):
|
|
14
|
+
@classmethod
|
|
15
|
+
def setUpClass(cls):
|
|
16
|
+
cls.res = P.parse_product_cards((FIX / "search.html").read_text(encoding="utf-8"))
|
|
17
|
+
|
|
18
|
+
def test_total(self):
|
|
19
|
+
self.assertEqual(self.res["total"], 207)
|
|
20
|
+
|
|
21
|
+
def test_card_count_and_first(self):
|
|
22
|
+
self.assertEqual(len(self.res["products"]), 10)
|
|
23
|
+
first = self.res["products"][0]
|
|
24
|
+
self.assertEqual(first["title"], "Mis Uht Süt Yarım Yağlı 1 L")
|
|
25
|
+
self.assertEqual(first["id"], "5834")
|
|
26
|
+
self.assertEqual(first["price_try"], 43.5)
|
|
27
|
+
self.assertTrue(first["url"].endswith("-p-5834"))
|
|
28
|
+
|
|
29
|
+
def test_prices(self):
|
|
30
|
+
by_id = {p["id"]: p for p in self.res["products"]}
|
|
31
|
+
self.assertEqual(by_id["7796"]["price_try"], 88.5)
|
|
32
|
+
self.assertEqual(by_id["5179"]["price_try"], 36.0)
|
|
33
|
+
|
|
34
|
+
def test_stats(self):
|
|
35
|
+
s = P.price_stats(self.res["products"])
|
|
36
|
+
self.assertEqual(s["count"], 10)
|
|
37
|
+
self.assertEqual(s["min_try"], 16.5)
|
|
38
|
+
self.assertEqual(s["max_try"], 88.5)
|
|
39
|
+
self.assertEqual(s["median_try"], 55.75)
|
|
40
|
+
self.assertEqual(s["mean_try"], 52.05)
|
|
41
|
+
|
|
42
|
+
def test_empty_stats(self):
|
|
43
|
+
self.assertEqual(P.price_stats([])["count"], 0)
|
|
44
|
+
self.assertIsNone(P.price_stats([])["min_try"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestProductPage(unittest.TestCase):
|
|
48
|
+
@classmethod
|
|
49
|
+
def setUpClass(cls):
|
|
50
|
+
cls.p = P.parse_product_page((FIX / "product.html").read_text(encoding="utf-8"))
|
|
51
|
+
|
|
52
|
+
def test_title_and_price(self):
|
|
53
|
+
self.assertEqual(self.p["title"], "Ahmad Tea Classic Earl Grey Demlik Poşet 40'lı")
|
|
54
|
+
self.assertEqual(self.p["price_try"], 149.0)
|
|
55
|
+
|
|
56
|
+
def test_no_title_raises(self):
|
|
57
|
+
with self.assertRaises(ValueError):
|
|
58
|
+
P.parse_product_page("<html><body>no h1</body></html>")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class TestHomeAndCampaigns(unittest.TestCase):
|
|
62
|
+
def test_categories(self):
|
|
63
|
+
cats = P.parse_categories((FIX / "home.html").read_text(encoding="utf-8"))
|
|
64
|
+
self.assertEqual(len(cats), 19)
|
|
65
|
+
by_id = {c["id"]: c for c in cats}
|
|
66
|
+
self.assertEqual(by_id["460"]["name"], "Süt & Süt Ürünleri")
|
|
67
|
+
self.assertEqual(by_id["20"]["slug"], "meyve-ve-sebze")
|
|
68
|
+
|
|
69
|
+
def test_promotions(self):
|
|
70
|
+
promos = P.parse_promotions((FIX / "kampanyalar.html").read_text(encoding="utf-8"))
|
|
71
|
+
self.assertEqual(len(promos), 2)
|
|
72
|
+
names = [p["name"] for p in promos]
|
|
73
|
+
self.assertIn("Kazandırır Alışverişin", names)
|
|
74
|
+
self.assertIn("Kasa Arkası Ürünler", names)
|
|
75
|
+
|
|
76
|
+
def test_price_parsing(self):
|
|
77
|
+
self.assertEqual(P.parse_price("43,50"), 43.5)
|
|
78
|
+
self.assertEqual(P.parse_price("1.234,56"), 1234.56)
|
|
79
|
+
self.assertIsNone(P.parse_price("fiyat yok"))
|
|
80
|
+
self.assertIsNone(P.parse_price(None))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
unittest.main()
|