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 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
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ veriapi