├── setup.py ├── .isort.cfg ├── setup.cfg ├── .pre-commit-config.yaml ├── pyproject.toml ├── LICENSE.txt ├── README.md ├── .gitignore └── diyanet.py /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | known_third_party = setuptools 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = diyanet 3 | version = 0.1.2 4 | url = https://github.com/isidentical/diyanet 5 | author = isidentical 6 | author_email = isidentical@gmail.com 7 | description = Diyanet API 8 | long_description = file: README.md 9 | long_description_content_type = text/markdown 10 | license-file = LICENSE.txt 11 | 12 | [options] 13 | py_modules = diyanet 14 | python_requires = >=3.7 15 | 16 | [bdist_wheel] 17 | universal = True 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/python/black 5 | rev: 19.10b0 6 | hooks: 7 | - id: black 8 | - repo: https://github.com/asottile/seed-isort-config 9 | rev: v1.9.3 10 | hooks: 11 | - id: seed-isort-config 12 | - repo: https://github.com/pre-commit/mirrors-isort 13 | rev: v4.3.21 14 | hooks: 15 | - id: isort 16 | additional_dependencies: [toml] 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | target-version = ['py38'] 4 | exclude = ''' 5 | ( 6 | /( 7 | \.eggs # exclude a few common directories in the 8 | | \.git # root of the project 9 | | \.hg 10 | | \.mypy_cache 11 | | \.tox 12 | | \.venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | | rare_cases 18 | )/ 19 | | foo.py # also separately exclude a file named foo.py in 20 | # the root of the project 21 | ) 22 | ''' 23 | 24 | [tool.isort] 25 | multi_line_output=4 26 | force_grid_wrap=0 27 | line_length=79 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Diyanet 2 | ~~~~~~~ 3 | 4 | Copyright (c) 2020 Batuhan Taskaya 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. In any case, this project should be retain the above copyright notice, 10 | and credits to project github page. 11 | 2. The name of the author can be used to endorse or promote products 12 | derived from this software with specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR 15 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 16 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 17 | NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 18 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 19 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 21 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 22 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Diyanet 2 | 3 | Python interface for internal API of Turkey's Presidency of Religious 4 | Affairs to get prayer times. 5 | 6 | ## API 7 | ### Units 8 | - `GeographicUnit`, base class for all geographic units. All units share these members 9 | * `idx`: `int` => Internal ID (to use in API) 10 | * `name`: `str` => Name of the country 11 | - `Country`, unit for countries 12 | - `State`, unit for states (if no states present in given country, this will be same with `Country`) 13 | * `country`: `Country` => A link to it's country 14 | - `Region`, unit for citites/regions 15 | * `url`: `str` => URL that points out to prayer times page for that specific region 16 | * `state`: `State` => A link to it's state 17 | * `country`: `Country` => A link to it's country 18 | - `PrayerTimes`, unit for prayer times of a day 19 | * `fajr`: `time` 20 | * `sunrise`: `time` 21 | * `dhuhr`: `time` 22 | * `asr`: `time` 23 | * `maghrib`: `time` 24 | * `isha`: `time` 25 | 26 | ### API 27 | All methods described below are members of `Diyanet` class 28 | - `get_countries`: `() -> Iterator[Country]` => Iterates through all available countries 29 | - `get_states`: `(country: Country) -> Iterator[State]` => Iterates through all available states 30 | - `get_regions`: `(state: State) -> Iterator[Region]` => Iterates through all available regions 31 | - `get_country` / `get_state`/ `get_region` => Takes a `name` (and depending on the context, a geographical unit that covers itself) and returns if it finds something that matches with given name. If there isn't any match, it raises a `ValueError`. 32 | - `get_times`: `(region: Region) -> PrayerTimes` => Returns prayer times for the current day 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /diyanet.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import os 5 | import shelve 6 | from argparse import ArgumentParser 7 | from dataclasses import asdict, dataclass, field 8 | from datetime import time 9 | from enum import IntEnum 10 | from functools import partial, partialmethod 11 | from html.parser import HTMLParser 12 | from pathlib import Path 13 | from typing import Callable, Dict, Iterator, Type, TypeVar 14 | from urllib.parse import urlencode 15 | from urllib.request import Request, urlopen 16 | 17 | BASE_URL = "https://namazvakitleri.diyanet.gov.tr" 18 | CACHE_PRIORITY = ("DIYANET_CACHE_HOME", "XDG_CACHE_HOME") 19 | 20 | RecordStates = IntEnum("RecordStates", "NAME VALUE") 21 | shadow_field = partial(field, repr=False) 22 | 23 | 24 | @dataclass 25 | class GeographicUnit: 26 | name: str 27 | idx: int 28 | 29 | 30 | @dataclass 31 | class Country(GeographicUnit): 32 | pass 33 | 34 | 35 | @dataclass 36 | class State(GeographicUnit): 37 | country: Country = shadow_field() 38 | 39 | 40 | @dataclass 41 | class Region(GeographicUnit): 42 | url: str 43 | country: Country = shadow_field() 44 | state: State = shadow_field() 45 | 46 | 47 | @dataclass 48 | class PrayerTimes: 49 | fajr: time 50 | sunrise: time 51 | dhuhr: time 52 | asr: time 53 | maghrib: time 54 | isha: time 55 | 56 | 57 | def _get_cache_dir() -> Path: 58 | for option in CACHE_PRIORITY: 59 | if option in os.environ: 60 | path = os.environ.get(option) 61 | else: 62 | path = "~/.cache" 63 | 64 | path = Path(path).expanduser() 65 | if path.exists(): 66 | path = path / "diyanet" 67 | path.mkdir(exist_ok=True) 68 | return path 69 | else: 70 | raise ValueError( 71 | f"Either one of these {', '.join(CACHE_PRIORITY)} environment" 72 | f"variables should point to a valid path, or '~/.cache' should " 73 | f"be available." 74 | ) 75 | 76 | 77 | class OptionParser(HTMLParser): 78 | def __init__(self, identifier: str, *args, **kwargs): 79 | super().__init__(*args, **kwargs) 80 | self.options = [] 81 | self.identifier = identifier 82 | self.record_options = False 83 | 84 | def handle_starttag(self, tag, _attr): 85 | attributes = dict(_attr) 86 | if ( 87 | tag == "select" 88 | and "class" in attributes 89 | and self.identifier in attributes["class"] 90 | ): 91 | self.record_options = True 92 | elif self.record_options and tag == "option": 93 | self.options.append([None, int(attributes["value"])]) 94 | 95 | def handle_data(self, data): 96 | if ( 97 | self.record_options 98 | and self.lasttag == "option" 99 | and self.options[-1][0] is None 100 | ): 101 | self.options[-1][0] = data.casefold() 102 | 103 | def handle_endtag(self, tag): 104 | if tag == "select" and self.record_options: 105 | self.record_options = False 106 | self.options.sort(key=lambda t: t[1]) 107 | 108 | 109 | UnitType = TypeVar("UnitType", Type[State], Type[Region]) 110 | 111 | 112 | class PrayerTimeParser(HTMLParser): 113 | def __init__(self, *args, **kwargs): 114 | super().__init__(*args, **kwargs) 115 | self.times = [] 116 | self.record_state = None 117 | 118 | def handle_starttag(self, tag, _attr): 119 | attributes = dict(_attr) 120 | if tag == "div" and "class" in attributes: 121 | if attributes["class"] == "tpt-title": 122 | self.record_state = RecordStates.NAME 123 | elif attributes["class"] == "tpt-time": 124 | self.record_state = RecordStates.VALUE 125 | 126 | def handle_data(self, data): 127 | if self.record_state is None: 128 | return 129 | 130 | if self.record_state is RecordStates.NAME and ( 131 | len(self.times) == 0 or self.times[-1][1] is not None 132 | ): 133 | self.times.append([data.strip(), None]) 134 | elif ( 135 | self.record_state is RecordStates.VALUE 136 | and self.times[-1][1] is None 137 | ): 138 | self.times[-1][1] = data 139 | else: 140 | self.times.pop() 141 | 142 | def handle_endtag(self, tag): 143 | self.record_state = None 144 | 145 | 146 | class Diyanet: 147 | def __init__( 148 | self, 149 | *, 150 | db_path: os.PathLike = _get_cache_dir() / "db", 151 | base_url: str = BASE_URL, 152 | ) -> None: 153 | self.base_url = base_url 154 | 155 | cache_db = shelve.open(os.fspath(db_path)) 156 | self.initalize_db(cache_db) 157 | 158 | def initalize_db(self, db: shelve.Shelf) -> None: 159 | for section in "page", "countries", "regions": 160 | if section not in db: 161 | initalizer = getattr(self, f"initalize_{section}", dict) 162 | db[section] = initalizer() 163 | setattr(self, f"_{section}_cache", db[section]) 164 | 165 | def initalize_countries(self) -> Dict[str, Country]: 166 | page = self.fetch("/tr-TR/home") 167 | country_parser = OptionParser(identifier="country-select") 168 | country_parser.feed(page) 169 | return { 170 | country: Country(country, idx) 171 | for country, idx in country_parser.options 172 | } 173 | 174 | def do_request(self, request: Request) -> str: 175 | address = request.get_full_url() 176 | if address in self._page_cache: 177 | return self._page_cache[address] 178 | 179 | with urlopen(request) as page: 180 | self._page_cache[address] = content = page.read().decode() 181 | 182 | return content 183 | 184 | def fetch(self, endpoint: str, **kwargs) -> str: 185 | request = Request(f"{self.base_url}{endpoint}?" + urlencode(kwargs)) 186 | return self.do_request(request) 187 | 188 | def get_countries(self) -> Iterator[Country]: 189 | yield from self._countries_cache.values() 190 | 191 | def get_states(self, country: Country) -> Iterator[State]: 192 | data = json.loads( 193 | self.fetch( 194 | "/tr-TR/home/GetRegList", 195 | ChangeType="country", 196 | CountryId=country.idx, 197 | ) 198 | ) 199 | for state in data["StateList"]: 200 | yield State(state["SehirAdiEn"], state["SehirID"], country) 201 | 202 | def get_regions(self, state: State) -> Iterator[Region]: 203 | data = json.loads( 204 | self.fetch( 205 | "/tr-TR/home/GetRegList", 206 | ChangeType="state", 207 | CountryId=state.country.idx, 208 | StateId=state.idx, 209 | ) 210 | ) 211 | for region in data["StateRegionList"]: 212 | yield Region( 213 | region["IlceAdiEn"], 214 | region["IlceID"], 215 | region["IlceUrl"], 216 | state.country, 217 | state, 218 | ) 219 | 220 | def get_country(self, name: str) -> Country: 221 | for country in self.get_countries(): 222 | if name.casefold() == country.name.casefold(): 223 | return country 224 | else: 225 | raise ValueError(f"Unknown/unsupported country: '{name}'") 226 | 227 | def _geographic_search( 228 | self, unit: UnitType, arg: GeographicUnit, name: str 229 | ) -> UnitType: 230 | unit_name = unit.__name__.lower() 231 | lister = getattr(self, f"get_{unit_name}s") 232 | for listing in lister(arg): 233 | if name.casefold() == listing.name.casefold(): 234 | return listing 235 | else: 236 | raise ValueError(f"Unknown/unsupported {unit_name}: '{name}'") 237 | 238 | get_state = partialmethod(_geographic_search, State) 239 | get_region = partialmethod(_geographic_search, Region) 240 | 241 | def get_times(self, region: Region) -> PrayerTimes: 242 | page = self.fetch(region.url) 243 | parser = PrayerTimeParser() 244 | parser.feed(page) 245 | times = dict(parser.times) 246 | return PrayerTimes( 247 | time.fromisoformat(times["İmsak"]), 248 | time.fromisoformat(times["Güneş"]), 249 | time.fromisoformat(times["Öğle"]), 250 | time.fromisoformat(times["İkindi"]), 251 | time.fromisoformat(times["Akşam"]), 252 | time.fromisoformat(times["Yatsı"]), 253 | ) 254 | 255 | 256 | def main(): 257 | parser = ArgumentParser() 258 | parser.add_argument("country") 259 | parser.add_argument("state") 260 | parser.add_argument("region") 261 | options = parser.parse_args() 262 | 263 | connector = Diyanet() 264 | country = connector.get_country(options.country) 265 | state = connector.get_state(country, options.state) 266 | region = connector.get_region(state, options.region) 267 | for key, value in asdict(connector.get_times(region)).items(): 268 | print(key.title(), "===>", value) 269 | 270 | 271 | if __name__ == "__main__": 272 | main() 273 | --------------------------------------------------------------------------------