veriapi 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.
- veriapi-0.1.0/LICENSE +21 -0
- veriapi-0.1.0/PKG-INFO +117 -0
- veriapi-0.1.0/README.md +102 -0
- veriapi-0.1.0/pyproject.toml +25 -0
- veriapi-0.1.0/setup.cfg +4 -0
- veriapi-0.1.0/tests/test_veriapi.py +252 -0
- veriapi-0.1.0/veriapi/__init__.py +7 -0
- veriapi-0.1.0/veriapi/istemci.py +223 -0
- veriapi-0.1.0/veriapi/py.typed +0 -0
- veriapi-0.1.0/veriapi/webhook.py +53 -0
- veriapi-0.1.0/veriapi.egg-info/PKG-INFO +117 -0
- veriapi-0.1.0/veriapi.egg-info/SOURCES.txt +12 -0
- veriapi-0.1.0/veriapi.egg-info/dependency_links.txt +1 -0
- veriapi-0.1.0/veriapi.egg-info/top_level.txt +1 -0
veriapi-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VeriApi.Pro
|
|
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.
|
veriapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veriapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: VeriApi.Pro resmî Python istemcisi: döviz, hava, namaz, deprem, il/ilçe
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://veriapi.pro
|
|
7
|
+
Project-URL: Documentation, https://veriapi.pro/dokumantasyon
|
|
8
|
+
Project-URL: Source, https://github.com/veriapipro/veriapi-python
|
|
9
|
+
Project-URL: Issues, https://github.com/veriapipro/veriapi-python/issues
|
|
10
|
+
Keywords: veriapi,turkiye,doviz,hava,namaz,deprem
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# VeriApi.Pro Python SDK: döviz kuru, namaz vakti, hava durumu ve deprem API'si
|
|
17
|
+
|
|
18
|
+
Python ile döviz kurlarını, namaz vakitlerini, hava durumunu, son depremleri ve il/ilçe listesini tek API anahtarıyla çekin.
|
|
19
|
+
|
|
20
|
+
Official Python client for [VeriApi.Pro](https://veriapi.pro/?ref=github): Turkish exchange rates, prayer times, weather, earthquakes and cities.
|
|
21
|
+
|
|
22
|
+
Python 3.9+, bağımlılık yok.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install veriapi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Hızlı başlangıç
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from veriapi import VeriApi, VeriApiHatasi
|
|
32
|
+
|
|
33
|
+
api = VeriApi("va_ANAHTARINIZ")
|
|
34
|
+
|
|
35
|
+
sonuc = api.doviz("USD,EUR")
|
|
36
|
+
print(sonuc.veri["kurlar"])
|
|
37
|
+
print(sonuc.kaynak[0]["atif"]) # veriyi gösterdiğiniz yerde görünür yazın
|
|
38
|
+
print(sonuc.guncelleme, sonuc.bayat)
|
|
39
|
+
print(sonuc.kota.gunluk_kalan, sonuc.kota.dakikalik_kalan, sonuc.kota.istek_maliyeti)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Yöntemler
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
api.doviz(["USD", "EUR"], tur="referans") # tur: anlik | referans
|
|
46
|
+
api.hava(il="duzce", ilce="akcakoca") # ya da konum="duzce-akcakoca", ya da enlem=, boylam=
|
|
47
|
+
api.namaz(il="ankara", tarih="2026-11-05", gun=7)
|
|
48
|
+
api.depremler(limit=10, min_buyukluk=4.0, saat=24, il="izmir", yurtici=True)
|
|
49
|
+
api.iller()
|
|
50
|
+
api.ilceler("duzce") # ad, slug ya da plaka
|
|
51
|
+
|
|
52
|
+
api.get("/v1/takvim/tatiller", {"yil": 2026}) # diğer tüm uçlar
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Seçenekler
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
api = VeriApi(
|
|
59
|
+
"va_ANAHTARINIZ",
|
|
60
|
+
zaman_asimi=10.0, # istek başına saniye
|
|
61
|
+
yeniden_deneme=2, # 429'da en çok kaç kez daha; 0 kapatır
|
|
62
|
+
azami_bekleme=60.0, # bundan uzun Retry-After beklenmez, hata hemen fırlatılır
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Hatalar
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
try:
|
|
70
|
+
api.namaz(il="duzce", gun=99)
|
|
71
|
+
except VeriApiHatasi as hata:
|
|
72
|
+
print(hata.kod, hata.mesaj, hata.http_durumu)
|
|
73
|
+
print(hata.alanlar) # 422: [{"alan": "gun", "mesaj": "..."}]
|
|
74
|
+
print(hata.retry_after) # 429: saniye
|
|
75
|
+
print(hata.zaman_asimi) # zaman aşımında True; bağlantı hatasında kod ve http_durumu 0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Webhook doğrulama
|
|
79
|
+
|
|
80
|
+
Ham gövdeyi kullanın; JSON'u ayrıştırıp yeniden üretirseniz imza tutmaz.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from flask import Flask, abort, request
|
|
84
|
+
from veriapi import imza_dogrula
|
|
85
|
+
|
|
86
|
+
app = Flask(__name__)
|
|
87
|
+
|
|
88
|
+
@app.post("/webhook")
|
|
89
|
+
def webhook():
|
|
90
|
+
gecerli = imza_dogrula(
|
|
91
|
+
request.get_data(),
|
|
92
|
+
request.headers.get("X-VeriApi-Imza"),
|
|
93
|
+
request.headers.get("X-VeriApi-Zaman"),
|
|
94
|
+
"WEBHOOK_GIZLI_ANAHTARINIZ",
|
|
95
|
+
)
|
|
96
|
+
if not gecerli:
|
|
97
|
+
abort(401)
|
|
98
|
+
olay = request.headers.get("X-VeriApi-Olay")
|
|
99
|
+
print(olay, request.get_json())
|
|
100
|
+
return "", 204
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`tolerans=300` saniyeden eski ya da ileri tarihli imza reddedilir; `tolerans=0` kapatır.
|
|
104
|
+
|
|
105
|
+
## Test
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python3 -m unittest discover -s tests
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Bağlantılar
|
|
112
|
+
|
|
113
|
+
- Ücretsiz anahtar ve canlı deneme: <https://veriapi.pro/?ref=github>
|
|
114
|
+
- Dokümantasyon: <https://veriapi.pro/dokumantasyon>
|
|
115
|
+
- OpenAPI: <https://api.veriapi.pro/v1/openapi.json>
|
|
116
|
+
|
|
117
|
+
Lisans: MIT
|
veriapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# VeriApi.Pro Python SDK: döviz kuru, namaz vakti, hava durumu ve deprem API'si
|
|
2
|
+
|
|
3
|
+
Python ile döviz kurlarını, namaz vakitlerini, hava durumunu, son depremleri ve il/ilçe listesini tek API anahtarıyla çekin.
|
|
4
|
+
|
|
5
|
+
Official Python client for [VeriApi.Pro](https://veriapi.pro/?ref=github): Turkish exchange rates, prayer times, weather, earthquakes and cities.
|
|
6
|
+
|
|
7
|
+
Python 3.9+, bağımlılık yok.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install veriapi
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Hızlı başlangıç
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from veriapi import VeriApi, VeriApiHatasi
|
|
17
|
+
|
|
18
|
+
api = VeriApi("va_ANAHTARINIZ")
|
|
19
|
+
|
|
20
|
+
sonuc = api.doviz("USD,EUR")
|
|
21
|
+
print(sonuc.veri["kurlar"])
|
|
22
|
+
print(sonuc.kaynak[0]["atif"]) # veriyi gösterdiğiniz yerde görünür yazın
|
|
23
|
+
print(sonuc.guncelleme, sonuc.bayat)
|
|
24
|
+
print(sonuc.kota.gunluk_kalan, sonuc.kota.dakikalik_kalan, sonuc.kota.istek_maliyeti)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Yöntemler
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
api.doviz(["USD", "EUR"], tur="referans") # tur: anlik | referans
|
|
31
|
+
api.hava(il="duzce", ilce="akcakoca") # ya da konum="duzce-akcakoca", ya da enlem=, boylam=
|
|
32
|
+
api.namaz(il="ankara", tarih="2026-11-05", gun=7)
|
|
33
|
+
api.depremler(limit=10, min_buyukluk=4.0, saat=24, il="izmir", yurtici=True)
|
|
34
|
+
api.iller()
|
|
35
|
+
api.ilceler("duzce") # ad, slug ya da plaka
|
|
36
|
+
|
|
37
|
+
api.get("/v1/takvim/tatiller", {"yil": 2026}) # diğer tüm uçlar
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Seçenekler
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
api = VeriApi(
|
|
44
|
+
"va_ANAHTARINIZ",
|
|
45
|
+
zaman_asimi=10.0, # istek başına saniye
|
|
46
|
+
yeniden_deneme=2, # 429'da en çok kaç kez daha; 0 kapatır
|
|
47
|
+
azami_bekleme=60.0, # bundan uzun Retry-After beklenmez, hata hemen fırlatılır
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Hatalar
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
try:
|
|
55
|
+
api.namaz(il="duzce", gun=99)
|
|
56
|
+
except VeriApiHatasi as hata:
|
|
57
|
+
print(hata.kod, hata.mesaj, hata.http_durumu)
|
|
58
|
+
print(hata.alanlar) # 422: [{"alan": "gun", "mesaj": "..."}]
|
|
59
|
+
print(hata.retry_after) # 429: saniye
|
|
60
|
+
print(hata.zaman_asimi) # zaman aşımında True; bağlantı hatasında kod ve http_durumu 0
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Webhook doğrulama
|
|
64
|
+
|
|
65
|
+
Ham gövdeyi kullanın; JSON'u ayrıştırıp yeniden üretirseniz imza tutmaz.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from flask import Flask, abort, request
|
|
69
|
+
from veriapi import imza_dogrula
|
|
70
|
+
|
|
71
|
+
app = Flask(__name__)
|
|
72
|
+
|
|
73
|
+
@app.post("/webhook")
|
|
74
|
+
def webhook():
|
|
75
|
+
gecerli = imza_dogrula(
|
|
76
|
+
request.get_data(),
|
|
77
|
+
request.headers.get("X-VeriApi-Imza"),
|
|
78
|
+
request.headers.get("X-VeriApi-Zaman"),
|
|
79
|
+
"WEBHOOK_GIZLI_ANAHTARINIZ",
|
|
80
|
+
)
|
|
81
|
+
if not gecerli:
|
|
82
|
+
abort(401)
|
|
83
|
+
olay = request.headers.get("X-VeriApi-Olay")
|
|
84
|
+
print(olay, request.get_json())
|
|
85
|
+
return "", 204
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`tolerans=300` saniyeden eski ya da ileri tarihli imza reddedilir; `tolerans=0` kapatır.
|
|
89
|
+
|
|
90
|
+
## Test
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
python3 -m unittest discover -s tests
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Bağlantılar
|
|
97
|
+
|
|
98
|
+
- Ücretsiz anahtar ve canlı deneme: <https://veriapi.pro/?ref=github>
|
|
99
|
+
- Dokümantasyon: <https://veriapi.pro/dokumantasyon>
|
|
100
|
+
- OpenAPI: <https://api.veriapi.pro/v1/openapi.json>
|
|
101
|
+
|
|
102
|
+
Lisans: MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "veriapi"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "VeriApi.Pro resmî Python istemcisi: döviz, hava, namaz, deprem, il/ilçe"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
dependencies = []
|
|
13
|
+
keywords = ["veriapi", "turkiye", "doviz", "hava", "namaz", "deprem"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://veriapi.pro"
|
|
17
|
+
Documentation = "https://veriapi.pro/dokumantasyon"
|
|
18
|
+
Source = "https://github.com/veriapipro/veriapi-python"
|
|
19
|
+
Issues = "https://github.com/veriapipro/veriapi-python/issues"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools]
|
|
22
|
+
packages = ["veriapi"]
|
|
23
|
+
|
|
24
|
+
[tool.setuptools.package-data]
|
|
25
|
+
veriapi = ["py.typed"]
|
veriapi-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Sahte yerel HTTP sunucusuyla birim testleri. Çalıştırma: python3 -m unittest discover -s tests"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import unittest
|
|
9
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from urllib.parse import parse_qs, urlsplit
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
13
|
+
|
|
14
|
+
from veriapi import VeriApi, VeriApiHatasi, imza_dogrula # noqa: E402
|
|
15
|
+
|
|
16
|
+
KOTA = {"X-RateLimit-Limit-Day": "1000", "X-RateLimit-Remaining-Day": "999", "X-RateLimit-Limit-Minute": "20",
|
|
17
|
+
"X-RateLimit-Remaining-Minute": "19", "X-Istek-Maliyeti": "1"}
|
|
18
|
+
ZARF = {"veri": {"kurlar": [{"kod": "USD"}]},
|
|
19
|
+
"kaynak": [{"kod": "ecb", "ad": "ECB", "lisans": "x", "atif": "y", "url": "z"}],
|
|
20
|
+
"guncelleme": "2026-11-05T10:00:00", "bayat": True}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Sunucu(BaseHTTPRequestHandler):
|
|
24
|
+
def do_GET(self):
|
|
25
|
+
self.server.istekler.append((self.path, {k.lower(): v for k, v in self.headers.items()}))
|
|
26
|
+
durum, basliklar, govde, gecikme = self.server.yanitlar.pop(0) if self.server.yanitlar else (200, KOTA, ZARF, 0)
|
|
27
|
+
if gecikme:
|
|
28
|
+
time.sleep(gecikme)
|
|
29
|
+
ham = govde if isinstance(govde, bytes) else json.dumps(govde).encode()
|
|
30
|
+
self.send_response(durum)
|
|
31
|
+
for k, v in basliklar.items():
|
|
32
|
+
self.send_header(k, v)
|
|
33
|
+
self.send_header("Content-Length", str(len(ham)))
|
|
34
|
+
self.end_headers()
|
|
35
|
+
try:
|
|
36
|
+
self.wfile.write(ham)
|
|
37
|
+
except BrokenPipeError: # zaman aşımı testinde istemci çoktan kapatmıştır
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def log_message(self, *args):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Temel(unittest.TestCase):
|
|
45
|
+
def setUp(self):
|
|
46
|
+
self.sunucu = ThreadingHTTPServer(("127.0.0.1", 0), Sunucu)
|
|
47
|
+
self.sunucu.istekler, self.sunucu.yanitlar = [], []
|
|
48
|
+
threading.Thread(target=self.sunucu.serve_forever, args=(0.01,), daemon=True).start()
|
|
49
|
+
self.addCleanup(self.sunucu.server_close)
|
|
50
|
+
self.addCleanup(self.sunucu.shutdown)
|
|
51
|
+
self.beklemeler = []
|
|
52
|
+
self.api = self.istemci()
|
|
53
|
+
|
|
54
|
+
def istemci(self, **secenekler):
|
|
55
|
+
api = VeriApi("va_test", temel_adres=f"http://127.0.0.1:{self.sunucu.server_port}/", **secenekler)
|
|
56
|
+
api._uyu = self.beklemeler.append
|
|
57
|
+
return api
|
|
58
|
+
|
|
59
|
+
def yanitla(self, *yanitlar):
|
|
60
|
+
self.sunucu.yanitlar.extend((*y, 0) if len(y) == 3 else y for y in yanitlar)
|
|
61
|
+
|
|
62
|
+
def son(self):
|
|
63
|
+
yol, basliklar = self.sunucu.istekler[-1]
|
|
64
|
+
parca = urlsplit(yol)
|
|
65
|
+
return parca.path, parse_qs(parca.query), basliklar
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Basari(Temel):
|
|
69
|
+
def test_zarf_ve_kota(self):
|
|
70
|
+
sonuc = self.api.get("/v1/doviz", {"kod": "USD", "bos": None})
|
|
71
|
+
self.assertEqual(sonuc.veri, ZARF["veri"])
|
|
72
|
+
self.assertEqual(sonuc.kaynak[0]["kod"], "ecb")
|
|
73
|
+
self.assertEqual(sonuc.guncelleme, ZARF["guncelleme"])
|
|
74
|
+
self.assertTrue(sonuc.bayat)
|
|
75
|
+
self.assertEqual((sonuc.kota.gunluk_limit, sonuc.kota.gunluk_kalan, sonuc.kota.dakikalik_limit,
|
|
76
|
+
sonuc.kota.dakikalik_kalan, sonuc.kota.istek_maliyeti), (1000, 999, 20, 19, 1))
|
|
77
|
+
yol, sorgu, basliklar = self.son()
|
|
78
|
+
self.assertEqual((yol, sorgu), ("/v1/doviz", {"kod": ["USD"]}))
|
|
79
|
+
self.assertEqual(basliklar["x-api-key"], "va_test")
|
|
80
|
+
self.assertNotIn("anahtar", self.sunucu.istekler[-1][0])
|
|
81
|
+
|
|
82
|
+
def test_kisa_yol_ve_zarfsiz_govde(self):
|
|
83
|
+
self.yanitla((200, {}, {"durum": "ok"}))
|
|
84
|
+
sonuc = self.api.get("durum")
|
|
85
|
+
self.assertEqual(self.son()[0], "/v1/durum")
|
|
86
|
+
self.assertEqual(sonuc.veri, {"durum": "ok"})
|
|
87
|
+
self.assertEqual(sonuc.kaynak, [])
|
|
88
|
+
self.assertIsNone(sonuc.kota.gunluk_kalan)
|
|
89
|
+
|
|
90
|
+
def test_kolaylik_yontemleri(self):
|
|
91
|
+
api = self.api
|
|
92
|
+
api.doviz(["USD", "EUR"], tur="referans")
|
|
93
|
+
self.assertEqual(self.son()[:2], ("/v1/doviz", {"kod": ["USD,EUR"], "tur": ["referans"]}))
|
|
94
|
+
api.hava(il="Düzce", ilce="Akçakoca")
|
|
95
|
+
self.assertEqual(self.son()[:2], ("/v1/hava", {"il": ["Düzce"], "ilce": ["Akçakoca"]}))
|
|
96
|
+
api.hava(enlem=0.0, boylam=32.5)
|
|
97
|
+
self.assertEqual(self.son()[:2], ("/v1/hava", {"enlem": ["0.0"], "boylam": ["32.5"]}))
|
|
98
|
+
api.namaz(konum="duzce-akcakoca", tarih="2026-11-05", gun=7)
|
|
99
|
+
self.assertEqual(self.son()[:2], ("/v1/namaz", {"konum": ["duzce-akcakoca"], "tarih": ["2026-11-05"], "gun": ["7"]}))
|
|
100
|
+
api.depremler(limit=5, min_buyukluk=4.5, saat=24, il=81, yurtici=True)
|
|
101
|
+
self.assertEqual(self.son()[:2], ("/v1/deprem/son", {"limit": ["5"], "min_buyukluk": ["4.5"], "saat": ["24"],
|
|
102
|
+
"il": ["81"], "yurtici": ["true"]}))
|
|
103
|
+
api.iller()
|
|
104
|
+
self.assertEqual(self.son()[0], "/v1/konumlar/iller")
|
|
105
|
+
api.ilceler("Düzce/x")
|
|
106
|
+
self.assertIn("/v1/konumlar/iller/D%C3%BCzce%2Fx/ilceler", self.sunucu.istekler[-1][0])
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Hatalar(Temel):
|
|
110
|
+
def test_tipli_hata(self):
|
|
111
|
+
self.yanitla((404, KOTA, {"hata": {"kod": 404, "mesaj": "İl bulunamadı: x"}}))
|
|
112
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
113
|
+
self.api.ilceler("x")
|
|
114
|
+
hata = baglam.exception
|
|
115
|
+
self.assertEqual((hata.kod, hata.mesaj, hata.http_durumu), (404, "İl bulunamadı: x", 404))
|
|
116
|
+
self.assertEqual(str(hata), "İl bulunamadı: x")
|
|
117
|
+
self.assertEqual(hata.kota.gunluk_kalan, 999)
|
|
118
|
+
|
|
119
|
+
def test_422_alanlar(self):
|
|
120
|
+
alanlar = [{"alan": "gun", "mesaj": "çok büyük"}]
|
|
121
|
+
self.yanitla((422, {}, {"hata": {"kod": 422, "mesaj": {"metin": "Geçersiz parametre", "alanlar": alanlar}}}))
|
|
122
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
123
|
+
self.api.namaz(il="duzce", gun=99)
|
|
124
|
+
self.assertEqual(baglam.exception.mesaj, "Geçersiz parametre")
|
|
125
|
+
self.assertEqual(baglam.exception.alanlar, alanlar)
|
|
126
|
+
|
|
127
|
+
def test_json_olmayan_hata(self):
|
|
128
|
+
self.yanitla((502, {"Content-Type": "text/html"}, b"<html>Bad Gateway</html>"))
|
|
129
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
130
|
+
self.api.iller()
|
|
131
|
+
self.assertEqual((baglam.exception.kod, baglam.exception.http_durumu), (502, 502))
|
|
132
|
+
|
|
133
|
+
def test_gecersiz_json_basari(self):
|
|
134
|
+
self.yanitla((200, {}, b"<html>"))
|
|
135
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
136
|
+
self.api.iller()
|
|
137
|
+
self.assertEqual((baglam.exception.kod, baglam.exception.http_durumu), (0, 200))
|
|
138
|
+
|
|
139
|
+
def test_yonlendirme_izlenmez(self):
|
|
140
|
+
self.yanitla((302, {"Location": "/v1/baska"}, b""))
|
|
141
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
142
|
+
self.api.iller()
|
|
143
|
+
self.assertEqual(baglam.exception.http_durumu, 302)
|
|
144
|
+
self.assertEqual(len(self.sunucu.istekler), 1)
|
|
145
|
+
|
|
146
|
+
def test_zaman_asimi(self):
|
|
147
|
+
self.yanitla((200, {}, ZARF, 0.5))
|
|
148
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
149
|
+
self.istemci(zaman_asimi=0.05).iller()
|
|
150
|
+
self.assertTrue(baglam.exception.zaman_asimi)
|
|
151
|
+
self.assertEqual((baglam.exception.kod, baglam.exception.http_durumu), (0, 0))
|
|
152
|
+
|
|
153
|
+
def test_baglanti_hatasi(self):
|
|
154
|
+
api = VeriApi("va_test", temel_adres="http://127.0.0.1:9")
|
|
155
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
156
|
+
api.iller()
|
|
157
|
+
self.assertFalse(baglam.exception.zaman_asimi)
|
|
158
|
+
self.assertEqual(baglam.exception.http_durumu, 0)
|
|
159
|
+
|
|
160
|
+
def test_bos_anahtar(self):
|
|
161
|
+
with self.assertRaises(ValueError):
|
|
162
|
+
VeriApi("")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class YenidenDeneme(Temel):
|
|
166
|
+
SINIR = {"hata": {"kod": 429, "mesaj": "Dakikalık istek sınırı aşıldı"}}
|
|
167
|
+
|
|
168
|
+
def test_retry_after_uyulur(self):
|
|
169
|
+
self.yanitla((429, {"Retry-After": "7"}, self.SINIR), (429, {"Retry-After": "3"}, self.SINIR))
|
|
170
|
+
self.assertTrue(self.api.iller().bayat)
|
|
171
|
+
self.assertEqual(self.beklemeler, [7.0, 3.0])
|
|
172
|
+
self.assertEqual(len(self.sunucu.istekler), 3)
|
|
173
|
+
|
|
174
|
+
def test_en_cok_iki_deneme(self):
|
|
175
|
+
self.yanitla(*[(429, {"Retry-After": "1"}, self.SINIR)] * 3)
|
|
176
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
177
|
+
self.api.iller()
|
|
178
|
+
self.assertEqual(baglam.exception.http_durumu, 429)
|
|
179
|
+
self.assertEqual(baglam.exception.retry_after, 1.0)
|
|
180
|
+
self.assertEqual(len(self.sunucu.istekler), 3)
|
|
181
|
+
|
|
182
|
+
def test_kapatilabilir(self):
|
|
183
|
+
self.yanitla((429, {"Retry-After": "1"}, self.SINIR))
|
|
184
|
+
with self.assertRaises(VeriApiHatasi):
|
|
185
|
+
self.istemci(yeniden_deneme=0).iller()
|
|
186
|
+
self.assertEqual(self.beklemeler, [])
|
|
187
|
+
self.assertEqual(len(self.sunucu.istekler), 1)
|
|
188
|
+
|
|
189
|
+
def test_uzun_bekleme_denenmez(self):
|
|
190
|
+
self.yanitla((429, {"Retry-After": "40000"}, self.SINIR))
|
|
191
|
+
with self.assertRaises(VeriApiHatasi) as baglam:
|
|
192
|
+
self.api.iller()
|
|
193
|
+
self.assertEqual(baglam.exception.retry_after, 40000.0)
|
|
194
|
+
self.assertEqual(self.beklemeler, [])
|
|
195
|
+
|
|
196
|
+
def test_baslik_yoksa_varsayilan(self):
|
|
197
|
+
self.yanitla((429, {}, self.SINIR))
|
|
198
|
+
self.api.iller()
|
|
199
|
+
self.assertEqual(self.beklemeler, [1.0])
|
|
200
|
+
|
|
201
|
+
def test_baska_hata_denenmez(self):
|
|
202
|
+
self.yanitla((503, {"Retry-After": "1"}, {"hata": {"kod": 503, "mesaj": "hazır değil"}}))
|
|
203
|
+
with self.assertRaises(VeriApiHatasi):
|
|
204
|
+
self.api.iller()
|
|
205
|
+
self.assertEqual(len(self.sunucu.istekler), 1)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Webhook(unittest.TestCase):
|
|
209
|
+
# Dört SDK'nın testlerinde aynı örnek
|
|
210
|
+
GIZLI = "whsec_ornek"
|
|
211
|
+
ZAMAN = "1762300800"
|
|
212
|
+
GOVDE = '{"olay":"deprem.yeni","veri":{"buyukluk":4.2,"il":"Düzce"}}'
|
|
213
|
+
IMZA = "sha256=c7be673a84807aac83b2d3f90ef21166c532c8006043a2558cefe9c3d839edab"
|
|
214
|
+
SIMDI = 1762300900
|
|
215
|
+
|
|
216
|
+
def dogrula(self, govde=GOVDE, imza=IMZA, zaman=ZAMAN, gizli=GIZLI, **secenekler):
|
|
217
|
+
secenekler.setdefault("simdi", self.SIMDI)
|
|
218
|
+
return imza_dogrula(govde, imza, zaman, gizli, **secenekler)
|
|
219
|
+
|
|
220
|
+
def test_gecerli(self):
|
|
221
|
+
self.assertTrue(self.dogrula())
|
|
222
|
+
self.assertTrue(self.dogrula(govde=self.GOVDE.encode()))
|
|
223
|
+
self.assertTrue(self.dogrula(imza=self.IMZA.upper().replace("SHA256", "sha256")))
|
|
224
|
+
|
|
225
|
+
def test_gecersiz(self):
|
|
226
|
+
self.assertFalse(self.dogrula(govde=self.GOVDE + " "))
|
|
227
|
+
self.assertFalse(self.dogrula(gizli="baska"))
|
|
228
|
+
self.assertFalse(self.dogrula(gizli=""))
|
|
229
|
+
self.assertFalse(self.dogrula(zaman="1762300801"))
|
|
230
|
+
self.assertFalse(self.dogrula(imza=self.IMZA[7:]))
|
|
231
|
+
self.assertFalse(self.dogrula(imza="sha256=zz"))
|
|
232
|
+
self.assertFalse(self.dogrula(imza="sha256=ğğ"))
|
|
233
|
+
self.assertFalse(self.dogrula(imza=None))
|
|
234
|
+
self.assertFalse(self.dogrula(zaman=None))
|
|
235
|
+
|
|
236
|
+
def test_tolerans(self):
|
|
237
|
+
self.assertFalse(self.dogrula(simdi=self.SIMDI + 300))
|
|
238
|
+
self.assertTrue(self.dogrula(simdi=self.SIMDI + 300, tolerans=0))
|
|
239
|
+
self.assertTrue(self.dogrula(simdi=self.SIMDI + 300, tolerans=1000))
|
|
240
|
+
|
|
241
|
+
def test_iso_zaman(self):
|
|
242
|
+
import hashlib
|
|
243
|
+
import hmac
|
|
244
|
+
zaman = "2026-11-05T00:00:00+03:00"
|
|
245
|
+
ozet = hmac.new(b"g", f"{zaman}.x".encode(), hashlib.sha256).hexdigest()
|
|
246
|
+
an = 1793826000 # 2026-11-04T21:00:00Z
|
|
247
|
+
self.assertTrue(imza_dogrula("x", f"sha256={ozet}", zaman, "g", simdi=an + 10))
|
|
248
|
+
self.assertFalse(imza_dogrula("x", f"sha256={ozet}", zaman, "g", simdi=an + 1000))
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
unittest.main()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""VeriApi.Pro resmî Python istemcisi (yalnızca standart kitaplık)."""
|
|
2
|
+
|
|
3
|
+
from .istemci import Kota, Sonuc, VeriApi, VeriApiHatasi
|
|
4
|
+
from .webhook import imza_dogrula
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__all__ = ["Kota", "Sonuc", "VeriApi", "VeriApiHatasi", "imza_dogrula", "__version__"]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""HTTP istemcisi: zarf çözme, tipli hata, 429 yeniden deneme, kolaylık yöntemleri."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import http.client
|
|
6
|
+
import json
|
|
7
|
+
import socket
|
|
8
|
+
import time
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.parse
|
|
11
|
+
import urllib.request
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Dict, Iterable, List, Mapping, Optional, Union
|
|
14
|
+
|
|
15
|
+
SURUM = "0.1.0"
|
|
16
|
+
VARSAYILAN_ADRES = "https://api.veriapi.pro"
|
|
17
|
+
VARSAYILAN_BEKLEME = 1.0 # 429'da Retry-After yoksa (saniye)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _tam(deger: Optional[str]) -> Optional[int]:
|
|
21
|
+
try:
|
|
22
|
+
return int(deger) if deger is not None else None
|
|
23
|
+
except ValueError:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Kota:
|
|
29
|
+
"""X-RateLimit-* ve X-Istek-Maliyeti başlıkları; başlık yoksa alan None."""
|
|
30
|
+
|
|
31
|
+
gunluk_limit: Optional[int] = None
|
|
32
|
+
gunluk_kalan: Optional[int] = None
|
|
33
|
+
dakikalik_limit: Optional[int] = None
|
|
34
|
+
dakikalik_kalan: Optional[int] = None
|
|
35
|
+
istek_maliyeti: Optional[int] = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def basliklardan(cls, basliklar: Any) -> "Kota":
|
|
39
|
+
return cls(
|
|
40
|
+
gunluk_limit=_tam(basliklar.get("X-RateLimit-Limit-Day")),
|
|
41
|
+
gunluk_kalan=_tam(basliklar.get("X-RateLimit-Remaining-Day")),
|
|
42
|
+
dakikalik_limit=_tam(basliklar.get("X-RateLimit-Limit-Minute")),
|
|
43
|
+
dakikalik_kalan=_tam(basliklar.get("X-RateLimit-Remaining-Minute")),
|
|
44
|
+
istek_maliyeti=_tam(basliklar.get("X-Istek-Maliyeti")),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class Sonuc:
|
|
50
|
+
"""Çözülmüş yanıt zarfı. Zarfsız uçlarda (/v1/durum) `veri` gövdenin tamamıdır."""
|
|
51
|
+
|
|
52
|
+
veri: Any
|
|
53
|
+
kaynak: List[Dict[str, Any]] = field(default_factory=list)
|
|
54
|
+
guncelleme: Optional[str] = None
|
|
55
|
+
bayat: bool = False
|
|
56
|
+
kota: Kota = field(default_factory=Kota)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class VeriApiHatasi(Exception):
|
|
60
|
+
"""API ya da bağlantı hatası.
|
|
61
|
+
|
|
62
|
+
kod: sunucunun `hata.kod` değeri; bağlantı ve zaman aşımında 0.
|
|
63
|
+
http_durumu: HTTP durum kodu; yanıt alınamadıysa 0.
|
|
64
|
+
alanlar: 422'de hatalı parametreler [{"alan", "mesaj"}].
|
|
65
|
+
retry_after: 429'da saniye. kota: yanıttaki kota başlıkları.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, kod: int, mesaj: str, http_durumu: int, *, alanlar: Optional[List[Dict[str, Any]]] = None,
|
|
69
|
+
retry_after: Optional[float] = None, kota: Optional[Kota] = None, zaman_asimi: bool = False):
|
|
70
|
+
super().__init__(mesaj)
|
|
71
|
+
self.kod = kod
|
|
72
|
+
self.mesaj = mesaj
|
|
73
|
+
self.http_durumu = http_durumu
|
|
74
|
+
self.alanlar = alanlar or []
|
|
75
|
+
self.retry_after = retry_after
|
|
76
|
+
self.kota = kota or Kota()
|
|
77
|
+
self.zaman_asimi = zaman_asimi
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _YonlendirmeYok(urllib.request.HTTPRedirectHandler):
|
|
81
|
+
"""X-API-Key başka bir adrese taşınmasın: yönlendirme izlenmez, 3xx hata olur."""
|
|
82
|
+
|
|
83
|
+
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _deger(deger: Any) -> str:
|
|
88
|
+
if isinstance(deger, bool):
|
|
89
|
+
return "true" if deger else "false"
|
|
90
|
+
if isinstance(deger, (list, tuple)):
|
|
91
|
+
return ",".join(str(d) for d in deger)
|
|
92
|
+
return str(deger)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _retry_after(deger: Optional[str]) -> Optional[float]:
|
|
96
|
+
try:
|
|
97
|
+
return max(0.0, float(deger)) if deger is not None else None
|
|
98
|
+
except ValueError:
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _hata(durum: int, govde: bytes, basliklar: Any) -> VeriApiHatasi:
|
|
103
|
+
kod, mesaj, alanlar = durum, f"Beklenmeyen yanıt (HTTP {durum})", None
|
|
104
|
+
try:
|
|
105
|
+
hata = json.loads(govde)["hata"]
|
|
106
|
+
kod = int(hata.get("kod", durum))
|
|
107
|
+
ham = hata.get("mesaj")
|
|
108
|
+
if isinstance(ham, dict): # 422: {"metin": "...", "alanlar": [...]}
|
|
109
|
+
mesaj = str(ham.get("metin") or mesaj)
|
|
110
|
+
alanlar = ham.get("alanlar")
|
|
111
|
+
elif ham:
|
|
112
|
+
mesaj = str(ham)
|
|
113
|
+
except (ValueError, KeyError, TypeError, AttributeError):
|
|
114
|
+
pass # JSON olmayan yanıt (vekil sunucu 502 sayfası vb.)
|
|
115
|
+
return VeriApiHatasi(kod, mesaj, durum, alanlar=alanlar, kota=Kota.basliklardan(basliklar),
|
|
116
|
+
retry_after=_retry_after(basliklar.get("Retry-After")))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class VeriApi:
|
|
120
|
+
"""VeriApi.Pro istemcisi.
|
|
121
|
+
|
|
122
|
+
yeniden_deneme: 429'da en çok kaç kez daha denensin (0 kapatır).
|
|
123
|
+
azami_bekleme: bundan uzun Retry-After beklenmez, hata hemen fırlatılır (günlük kota aşımında saatler sürebilir).
|
|
124
|
+
zaman_asimi: istek başına saniye.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, anahtar: str, *, temel_adres: str = VARSAYILAN_ADRES, zaman_asimi: float = 10.0,
|
|
128
|
+
yeniden_deneme: int = 2, azami_bekleme: float = 60.0):
|
|
129
|
+
if not anahtar:
|
|
130
|
+
raise ValueError("API anahtarı gerekli")
|
|
131
|
+
self._anahtar = anahtar
|
|
132
|
+
self._temel = temel_adres.rstrip("/")
|
|
133
|
+
self._zaman_asimi = zaman_asimi
|
|
134
|
+
self._yeniden_deneme = max(0, yeniden_deneme)
|
|
135
|
+
self._azami_bekleme = azami_bekleme
|
|
136
|
+
self._acici = urllib.request.build_opener(_YonlendirmeYok)
|
|
137
|
+
self._uyu = time.sleep
|
|
138
|
+
|
|
139
|
+
def get(self, yol: str, parametreler: Optional[Mapping[str, Any]] = None) -> Sonuc:
|
|
140
|
+
"""`yol` "/v1/doviz" ya da "doviz" olabilir. None değerli parametreler gönderilmez."""
|
|
141
|
+
url = self._url(yol, parametreler)
|
|
142
|
+
deneme = 0
|
|
143
|
+
while True:
|
|
144
|
+
durum, govde, basliklar = self._istek(url)
|
|
145
|
+
if durum != 429 or deneme >= self._yeniden_deneme:
|
|
146
|
+
break
|
|
147
|
+
bekle = _retry_after(basliklar.get("Retry-After"))
|
|
148
|
+
bekle = VARSAYILAN_BEKLEME if bekle is None else bekle
|
|
149
|
+
if bekle > self._azami_bekleme:
|
|
150
|
+
break
|
|
151
|
+
self._uyu(bekle)
|
|
152
|
+
deneme += 1
|
|
153
|
+
if not 200 <= durum < 300:
|
|
154
|
+
raise _hata(durum, govde, basliklar)
|
|
155
|
+
kota = Kota.basliklardan(basliklar)
|
|
156
|
+
try:
|
|
157
|
+
belge = json.loads(govde)
|
|
158
|
+
except ValueError:
|
|
159
|
+
raise VeriApiHatasi(0, "Yanıt JSON değil", durum, kota=kota) from None
|
|
160
|
+
if not isinstance(belge, dict) or "veri" not in belge:
|
|
161
|
+
return Sonuc(veri=belge, kota=kota)
|
|
162
|
+
return Sonuc(veri=belge["veri"], kaynak=belge.get("kaynak") or [], guncelleme=belge.get("guncelleme"),
|
|
163
|
+
bayat=bool(belge.get("bayat")), kota=kota)
|
|
164
|
+
|
|
165
|
+
def _url(self, yol: str, parametreler: Optional[Mapping[str, Any]]) -> str:
|
|
166
|
+
if not yol.startswith("/v1/"):
|
|
167
|
+
yol = "/v1/" + yol.lstrip("/")
|
|
168
|
+
temiz = {k: _deger(v) for k, v in (parametreler or {}).items() if v is not None}
|
|
169
|
+
if not temiz:
|
|
170
|
+
return self._temel + yol
|
|
171
|
+
ayrac = "&" if "?" in yol else "?"
|
|
172
|
+
return self._temel + yol + ayrac + urllib.parse.urlencode(temiz, quote_via=urllib.parse.quote)
|
|
173
|
+
|
|
174
|
+
def _istek(self, url: str) -> Any:
|
|
175
|
+
istek = urllib.request.Request(url, headers={
|
|
176
|
+
"X-API-Key": self._anahtar, "Accept": "application/json", "User-Agent": f"veriapi-python/{SURUM}",
|
|
177
|
+
})
|
|
178
|
+
try:
|
|
179
|
+
with self._acici.open(istek, timeout=self._zaman_asimi) as yanit:
|
|
180
|
+
return yanit.status, yanit.read(), yanit.headers
|
|
181
|
+
except urllib.error.HTTPError as hata:
|
|
182
|
+
with hata:
|
|
183
|
+
return hata.code, hata.read(), hata.headers
|
|
184
|
+
except (TimeoutError, socket.timeout) as hata:
|
|
185
|
+
raise self._zaman_asimi_hatasi() from hata
|
|
186
|
+
except (urllib.error.URLError, http.client.HTTPException, OSError) as hata:
|
|
187
|
+
if isinstance(getattr(hata, "reason", None), (TimeoutError, socket.timeout)):
|
|
188
|
+
raise self._zaman_asimi_hatasi() from hata
|
|
189
|
+
raise VeriApiHatasi(0, f"Bağlantı hatası: {hata}", 0) from hata
|
|
190
|
+
|
|
191
|
+
def _zaman_asimi_hatasi(self) -> VeriApiHatasi:
|
|
192
|
+
return VeriApiHatasi(0, f"Zaman aşımı ({self._zaman_asimi} sn)", 0, zaman_asimi=True)
|
|
193
|
+
|
|
194
|
+
# --- Kolaylık yöntemleri (parametreler api/app/routers/data.py ile aynı) ---
|
|
195
|
+
|
|
196
|
+
def doviz(self, kod: Union[str, Iterable[str], None] = None, tur: Optional[str] = None) -> Sonuc:
|
|
197
|
+
"""kod: "USD,EUR" ya da ["USD", "EUR"]; tur: "anlik" | "referans"."""
|
|
198
|
+
if kod is not None and not isinstance(kod, str):
|
|
199
|
+
kod = list(kod)
|
|
200
|
+
return self.get("/v1/doviz", {"kod": kod, "tur": tur})
|
|
201
|
+
|
|
202
|
+
def hava(self, *, konum: Optional[str] = None, il: Union[str, int, None] = None, ilce: Optional[str] = None,
|
|
203
|
+
enlem: Optional[float] = None, boylam: Optional[float] = None) -> Sonuc:
|
|
204
|
+
return self.get("/v1/hava", {"konum": konum, "il": il, "ilce": ilce, "enlem": enlem, "boylam": boylam})
|
|
205
|
+
|
|
206
|
+
def namaz(self, *, konum: Optional[str] = None, il: Union[str, int, None] = None, ilce: Optional[str] = None,
|
|
207
|
+
enlem: Optional[float] = None, boylam: Optional[float] = None, tarih: Optional[str] = None,
|
|
208
|
+
gun: Optional[int] = None) -> Sonuc:
|
|
209
|
+
"""tarih: YYYY-AA-GG (varsayılan bugün); gun: 1-31."""
|
|
210
|
+
return self.get("/v1/namaz", {"konum": konum, "il": il, "ilce": ilce, "enlem": enlem, "boylam": boylam,
|
|
211
|
+
"tarih": tarih, "gun": gun})
|
|
212
|
+
|
|
213
|
+
def depremler(self, *, limit: Optional[int] = None, min_buyukluk: Optional[float] = None, saat: Optional[int] = None,
|
|
214
|
+
il: Union[str, int, None] = None, yurtici: Optional[bool] = None) -> Sonuc:
|
|
215
|
+
return self.get("/v1/deprem/son", {"limit": limit, "min_buyukluk": min_buyukluk, "saat": saat, "il": il,
|
|
216
|
+
"yurtici": yurtici})
|
|
217
|
+
|
|
218
|
+
def iller(self) -> Sonuc:
|
|
219
|
+
return self.get("/v1/konumlar/iller")
|
|
220
|
+
|
|
221
|
+
def ilceler(self, il: Union[str, int]) -> Sonuc:
|
|
222
|
+
"""il: ad, slug ya da plaka."""
|
|
223
|
+
return self.get(f"/v1/konumlar/iller/{urllib.parse.quote(str(il), safe='')}/ilceler")
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Webhook imza doğrulama: imzalanan metin `<zaman>.<ham gövde>`, HMAC-SHA256 hex, başlık `sha256=<hex>`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import re
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Optional, Union
|
|
11
|
+
|
|
12
|
+
_IMZA = re.compile(r"sha256=([0-9a-fA-F]{64})")
|
|
13
|
+
_RFC3339 = re.compile(r"\d{4}-\d{2}-\d{2}T\S+(Z|[+-]\d{2}:\d{2})")
|
|
14
|
+
_SAYI = re.compile(r"\d+(\.\d+)?")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _saniye(zaman: str) -> Optional[float]:
|
|
18
|
+
"""Unix saniyesi ya da saat dilimli ISO 8601; başka biçim None."""
|
|
19
|
+
if _SAYI.fullmatch(zaman):
|
|
20
|
+
return float(zaman)
|
|
21
|
+
if _RFC3339.fullmatch(zaman):
|
|
22
|
+
try:
|
|
23
|
+
return datetime.fromisoformat(zaman.replace("Z", "+00:00")).timestamp()
|
|
24
|
+
except ValueError:
|
|
25
|
+
return None
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def imza_dogrula(govde: Union[bytes, str], imza: Optional[str], zaman: Optional[str], gizli: Union[bytes, str],
|
|
30
|
+
tolerans: float = 300, simdi: Optional[float] = None) -> bool:
|
|
31
|
+
"""X-VeriApi-Imza başlığını doğrular; geçersiz her girdide False döner, hata fırlatmaz.
|
|
32
|
+
|
|
33
|
+
govde: ayrıştırılmamış ham gövde (JSON'u yeniden üretmeyin, imza tutmaz).
|
|
34
|
+
imza, zaman: X-VeriApi-Imza ve X-VeriApi-Zaman başlıkları.
|
|
35
|
+
tolerans: yeniden oynatmaya karşı saniye cinsinden zaman farkı sınırı (0 kapatır).
|
|
36
|
+
"""
|
|
37
|
+
if not gizli or not imza or not zaman:
|
|
38
|
+
return False
|
|
39
|
+
eslesme = _IMZA.fullmatch(imza.strip())
|
|
40
|
+
if not eslesme:
|
|
41
|
+
return False
|
|
42
|
+
zaman = zaman.strip()
|
|
43
|
+
ham = govde.encode("utf-8") if isinstance(govde, str) else bytes(govde)
|
|
44
|
+
anahtar = gizli.encode("utf-8") if isinstance(gizli, str) else gizli
|
|
45
|
+
beklenen = hmac.new(anahtar, zaman.encode("utf-8") + b"." + ham, hashlib.sha256).hexdigest()
|
|
46
|
+
if not hmac.compare_digest(beklenen.encode("ascii"), eslesme.group(1).lower().encode("ascii")):
|
|
47
|
+
return False
|
|
48
|
+
if tolerans <= 0:
|
|
49
|
+
return True
|
|
50
|
+
saniye = _saniye(zaman)
|
|
51
|
+
if saniye is None:
|
|
52
|
+
return False
|
|
53
|
+
return abs((time.time() if simdi is None else simdi) - saniye) <= tolerans
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: veriapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: VeriApi.Pro resmî Python istemcisi: döviz, hava, namaz, deprem, il/ilçe
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://veriapi.pro
|
|
7
|
+
Project-URL: Documentation, https://veriapi.pro/dokumantasyon
|
|
8
|
+
Project-URL: Source, https://github.com/veriapipro/veriapi-python
|
|
9
|
+
Project-URL: Issues, https://github.com/veriapipro/veriapi-python/issues
|
|
10
|
+
Keywords: veriapi,turkiye,doviz,hava,namaz,deprem
|
|
11
|
+
Requires-Python: >=3.9
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# VeriApi.Pro Python SDK: döviz kuru, namaz vakti, hava durumu ve deprem API'si
|
|
17
|
+
|
|
18
|
+
Python ile döviz kurlarını, namaz vakitlerini, hava durumunu, son depremleri ve il/ilçe listesini tek API anahtarıyla çekin.
|
|
19
|
+
|
|
20
|
+
Official Python client for [VeriApi.Pro](https://veriapi.pro/?ref=github): Turkish exchange rates, prayer times, weather, earthquakes and cities.
|
|
21
|
+
|
|
22
|
+
Python 3.9+, bağımlılık yok.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install veriapi
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Hızlı başlangıç
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from veriapi import VeriApi, VeriApiHatasi
|
|
32
|
+
|
|
33
|
+
api = VeriApi("va_ANAHTARINIZ")
|
|
34
|
+
|
|
35
|
+
sonuc = api.doviz("USD,EUR")
|
|
36
|
+
print(sonuc.veri["kurlar"])
|
|
37
|
+
print(sonuc.kaynak[0]["atif"]) # veriyi gösterdiğiniz yerde görünür yazın
|
|
38
|
+
print(sonuc.guncelleme, sonuc.bayat)
|
|
39
|
+
print(sonuc.kota.gunluk_kalan, sonuc.kota.dakikalik_kalan, sonuc.kota.istek_maliyeti)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Yöntemler
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
api.doviz(["USD", "EUR"], tur="referans") # tur: anlik | referans
|
|
46
|
+
api.hava(il="duzce", ilce="akcakoca") # ya da konum="duzce-akcakoca", ya da enlem=, boylam=
|
|
47
|
+
api.namaz(il="ankara", tarih="2026-11-05", gun=7)
|
|
48
|
+
api.depremler(limit=10, min_buyukluk=4.0, saat=24, il="izmir", yurtici=True)
|
|
49
|
+
api.iller()
|
|
50
|
+
api.ilceler("duzce") # ad, slug ya da plaka
|
|
51
|
+
|
|
52
|
+
api.get("/v1/takvim/tatiller", {"yil": 2026}) # diğer tüm uçlar
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Seçenekler
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
api = VeriApi(
|
|
59
|
+
"va_ANAHTARINIZ",
|
|
60
|
+
zaman_asimi=10.0, # istek başına saniye
|
|
61
|
+
yeniden_deneme=2, # 429'da en çok kaç kez daha; 0 kapatır
|
|
62
|
+
azami_bekleme=60.0, # bundan uzun Retry-After beklenmez, hata hemen fırlatılır
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Hatalar
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
try:
|
|
70
|
+
api.namaz(il="duzce", gun=99)
|
|
71
|
+
except VeriApiHatasi as hata:
|
|
72
|
+
print(hata.kod, hata.mesaj, hata.http_durumu)
|
|
73
|
+
print(hata.alanlar) # 422: [{"alan": "gun", "mesaj": "..."}]
|
|
74
|
+
print(hata.retry_after) # 429: saniye
|
|
75
|
+
print(hata.zaman_asimi) # zaman aşımında True; bağlantı hatasında kod ve http_durumu 0
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Webhook doğrulama
|
|
79
|
+
|
|
80
|
+
Ham gövdeyi kullanın; JSON'u ayrıştırıp yeniden üretirseniz imza tutmaz.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from flask import Flask, abort, request
|
|
84
|
+
from veriapi import imza_dogrula
|
|
85
|
+
|
|
86
|
+
app = Flask(__name__)
|
|
87
|
+
|
|
88
|
+
@app.post("/webhook")
|
|
89
|
+
def webhook():
|
|
90
|
+
gecerli = imza_dogrula(
|
|
91
|
+
request.get_data(),
|
|
92
|
+
request.headers.get("X-VeriApi-Imza"),
|
|
93
|
+
request.headers.get("X-VeriApi-Zaman"),
|
|
94
|
+
"WEBHOOK_GIZLI_ANAHTARINIZ",
|
|
95
|
+
)
|
|
96
|
+
if not gecerli:
|
|
97
|
+
abort(401)
|
|
98
|
+
olay = request.headers.get("X-VeriApi-Olay")
|
|
99
|
+
print(olay, request.get_json())
|
|
100
|
+
return "", 204
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`tolerans=300` saniyeden eski ya da ileri tarihli imza reddedilir; `tolerans=0` kapatır.
|
|
104
|
+
|
|
105
|
+
## Test
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python3 -m unittest discover -s tests
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Bağlantılar
|
|
112
|
+
|
|
113
|
+
- Ücretsiz anahtar ve canlı deneme: <https://veriapi.pro/?ref=github>
|
|
114
|
+
- Dokümantasyon: <https://veriapi.pro/dokumantasyon>
|
|
115
|
+
- OpenAPI: <https://api.veriapi.pro/v1/openapi.json>
|
|
116
|
+
|
|
117
|
+
Lisans: MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tests/test_veriapi.py
|
|
5
|
+
veriapi/__init__.py
|
|
6
|
+
veriapi/istemci.py
|
|
7
|
+
veriapi/py.typed
|
|
8
|
+
veriapi/webhook.py
|
|
9
|
+
veriapi.egg-info/PKG-INFO
|
|
10
|
+
veriapi.egg-info/SOURCES.txt
|
|
11
|
+
veriapi.egg-info/dependency_links.txt
|
|
12
|
+
veriapi.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
veriapi
|