Konfiguratsiya va sirlar
Qattiq yozilgan qiymatlar muammosi
Loyihamizda allaqachon yashirin sozlamalar bor: ENG_KATTA_BAHO = 100, kerakli ustunlar ro'yxati,
chiqish fayl nomlari. Ular kodda turibdi, demak o'zgartirish uchun kodga tegish kerak.
Uchta muammo:
| Muammo | Misol |
|---|---|
| Muhitlar farq qiladi | Sinov bazasi va ishlab chiqarish bazasi — turli manzil |
| Sirlar kodga tushadi | Parol git ga yozilsa, u abadiy tarixda qoladi |
| Foydalanuvchi o'zgartira olmaydi | Baho 100 emas, 5 balli tizim bo'lsa-chi? |
12-omil qoidasi
Sanoat standarti oddiy: konfiguratsiya muhit o'zgaruvchilarida yashaydi. Kodda emas, faylda emas — jarayon muhitida.
Sabab: muhit o'zgaruvchisi har joyda bir xil ishlaydi (Docker, systemd, CI, lokal mashina) va u tasodifan git ga tushmaydi.
import os print("mavjud o'zgaruvchilardan namuna:") for nom in ["HOME", "LANG", "PATH"]: qiymat = os.environ.get(nom, "(yo'q)") print(f" {nom:6} = {qiymat[:45]}") print("\nyo'q o'zgaruvchi:", os.environ.get("HISOBOT_MAX_BAHO", "(yo'q — sukut ishlatiladi)"))
mavjud o'zgaruvchilardan namuna: HOME = /home/abulqosim LANG = C.UTF-8 PATH = /home/abulqosim/.local/bin:/usr/local/sbin:/u yo'q o'zgaruvchi: (yo'q — sukut ishlatiladi)
Sozlama sinfini yozamiz
shu faylni yaratingsrc/hisobot/sozlama.py
"""Konfiguratsiya: muhit o'zgaruvchilaridan o'qiladi, sukut qiymatlari bilan.""" import os from dataclasses import dataclass from pathlib import Path from .xatolar import SozlamaXato PREFIKS = "HISOBOT_" def _son(nom: str, sukut: int) -> int: xom = os.environ.get(PREFIKS + nom) if xom is None or xom == "": return sukut try: return int(xom) except ValueError: raise SozlamaXato(f"{PREFIKS}{nom} butun son bo'lishi kerak, berilgan: {xom!r}") from None def _mantiqiy(nom: str, sukut: bool) -> bool: xom = os.environ.get(PREFIKS + nom) if xom is None or xom == "": return sukut past = xom.strip().lower() if past in {"1", "true", "ha", "yes", "on"}: return True if past in {"0", "false", "yo'q", "yoq", "no", "off"}: return False raise SozlamaXato(f"{PREFIKS}{nom} mantiqiy qiymat bo'lishi kerak, berilgan: {xom!r}") @dataclass(frozen=True, slots=True) class Sozlama: max_baho: int = 100 min_baho: int = 0 chiqish_papka: Path = Path(".") qatiy: bool = False # True bo'lsa bitta xato ham butun ishni to'xtatadi @classmethod def muhitdan(cls) -> "Sozlama": s = cls( max_baho=_son("MAX_BAHO", 100), min_baho=_son("MIN_BAHO", 0), chiqish_papka=Path(os.environ.get(PREFIKS + "CHIQISH", ".")), qatiy=_mantiqiy("QATIY", False), ) s.tekshir() return s def tekshir(self) -> None: """Sozlama o'zaro mos kelishini tekshiradi — ISHGA TUSHISHDA, keyin emas.""" if self.min_baho >= self.max_baho: raise SozlamaXato( f"MIN_BAHO ({self.min_baho}) MAX_BAHO ({self.max_baho}) dan kichik bo'lishi kerak") if self.max_baho <= 0: raise SozlamaXato(f"MAX_BAHO musbat bo'lishi kerak, berilgan: {self.max_baho}")
.venv/bin/python3 -c " from hisobot.sozlama import Sozlama print('sukut bo\'yicha:', Sozlama.muhitdan()) " echo "--- muhit o'zgaruvchisi bilan ---" HISOBOT_MAX_BAHO=5 HISOBOT_QATIY=ha .venv/bin/python3 -c " from hisobot.sozlama import Sozlama print('o\'zgartirilgan:', Sozlama.muhitdan()) "
sukut bo'yicha: Sozlama(max_baho=100, min_baho=0, chiqish_papka=PosixPath('.'), qatiy=False)
--- muhit o'zgaruvchisi bilan ---
o'zgartirilgan: Sozlama(max_baho=5, min_baho=0, chiqish_papka=PosixPath('.'), qatiy=True)
Kod o'zgarmadi — faqat muhit.
Erta yiqiling
Eng muhim qoida: noto'g'ri sozlama bilan dastur umuman ishga tushmasin. Xato ishlash o'rtasida emas, birinchi soniyada chiqsin.
echo "=== noto'g'ri son ===" HISOBOT_MAX_BAHO=juda_kop .venv/bin/python3 -c " from hisobot.sozlama import Sozlama from hisobot.xatolar import SozlamaXato try: Sozlama.muhitdan() except SozlamaXato as e: print('XATO:', e) " echo "=== o'zaro mos emas ===" HISOBOT_MIN_BAHO=50 HISOBOT_MAX_BAHO=20 .venv/bin/python3 -c " from hisobot.sozlama import Sozlama from hisobot.xatolar import SozlamaXato try: Sozlama.muhitdan() except SozlamaXato as e: print('XATO:', e) " echo "=== noto'g'ri mantiqiy qiymat ===" HISOBOT_QATIY=balki .venv/bin/python3 -c " from hisobot.sozlama import Sozlama from hisobot.xatolar import SozlamaXato try: Sozlama.muhitdan() except SozlamaXato as e: print('XATO:', e) "
=== noto'g'ri son === XATO: HISOBOT_MAX_BAHO butun son bo'lishi kerak, berilgan: 'juda_kop' === o'zaro mos emas === XATO: MIN_BAHO (50) MAX_BAHO (20) dan kichik bo'lishi kerak === noto'g'ri mantiqiy qiymat === XATO: HISOBOT_QATIY mantiqiy qiymat bo'lishi kerak, berilgan: 'balki'
Har uch holatda aniq xabar. Taqqoslang: _mantiqiy bo'lmaganida HISOBOT_QATIY=false qiymati
True bo'lib ketardi, chunki bool("false") — rost:
print('bool("false") =', bool("false"), " <- eng klassik konfiguratsiya xatosi") print('bool("0") =', bool("0")) print('bool("") =', bool(""))
bool("false") = True <- eng klassik konfiguratsiya xatosi
bool("0") = True
bool("") = False
Sozlamani ishlatamiz
shu faylni yaratingsrc/hisobot/tekshiruv.py
"""Qoidalar: qator to'g'rimi yoki yo'q.""" from datetime import date from .sozlama import Sozlama ODATIY = Sozlama() def bahoni_tekshir(xom: str | None, sozlama: Sozlama | None = None) -> int: """Matnni bahoga aylantiradi. Xato bo'lsa sabab bilan ValueError.""" s = sozlama or ODATIY if xom is None: raise ValueError("baho ko'rsatilmagan") try: baho = int(xom) except (TypeError, ValueError): raise ValueError(f"baho son emas: {xom!r}") from None if not s.min_baho <= baho <= s.max_baho: raise ValueError( f"baho {s.min_baho}..{s.max_baho} oralig'ida bo'lishi kerak, berilgan: {baho}") return baho def sanani_tekshir(xom: str | None) -> str: """YYYY-MM-DD formatini tekshiradi va o'sha matnni qaytaradi.""" if not xom: raise ValueError("sana ko'rsatilmagan") try: date.fromisoformat(xom.strip()) except ValueError: raise ValueError(f"sana formati noto'g'ri (YYYY-MM-DD kutilgan): {xom!r}") from None return xom.strip()
.venv/bin/python3 -c " from hisobot.sozlama import Sozlama from hisobot.tekshiruv import bahoni_tekshir besh_balli = Sozlama(max_baho=5) for xom in ['4', '85']: for nom, s in [('100 balli', None), ('5 balli', besh_balli)]: try: print(f' {xom!r:5} {nom:10} -> {bahoni_tekshir(xom, s)}') except ValueError as e: print(f' {xom!r:5} {nom:10} -> XATO: {e}') "
'4' 100 balli -> 4 '4' 5 balli -> 4 '85' 100 balli -> 85 '85' 5 balli -> XATO: baho 0..5 oralig'ida bo'lishi kerak, berilgan: 85
Bir xil kod, ikki xil qoida. Sozlama argument sifatida uzatildi — global holat emas.
:::tip Sozlamani global qilmang
from .sozlama import SOZLAMA kabi global obyekt oson ko'rinadi, lekin testni qiyinlashtiradi:
har test global holatni o'zgartirishi va boshqasiga ta'sir qilishi mumkin. Sozlamani argument
sifatida uzating; sukut qiymati bo'lsa yetarli.
:::
Sirlar: parol va tokenlar
Sozlama va sir — boshqa narsalar. Sir ochilib qolsa zarar keltiradi.
import os def xavfsiz_koreanish(qiymat, oxiri=4): """Sirni jurnalda ko'rsatish uchun niqoblaydi.""" if not qiymat: return "(yo'q)" if len(qiymat) <= oxiri: return "*" * len(qiymat) return "*" * (len(qiymat) - oxiri) + qiymat[-oxiri:] for sir in ["sk-abc123def456ghi789", "qisqa", "12345678"]: print(f" {sir!r:26} -> {xavfsiz_koreanish(sir)}")
'sk-abc123def456ghi789' -> *****************i789 'qisqa' -> *isqa '12345678' -> ****5678
Qoidalar:
| Qoida | Sabab |
|---|---|
| Sirni kodga yozmang | Git tarixida abadiy qoladi, o'chirish qiyin |
.env ni .gitignore ga qo'ying |
Lokal fayl tasodifan yuklanmasin |
.env.namuna yozing |
Kerakli o'zgaruvchilar ro'yxati, qiymatlarsiz |
| Jurnalga niqoblab yozing | Jurnal boshqa tizimga yuboriladi |
| Ochilgan sirni almashtiring | O'chirish yetarli emas — u allaqachon ko'rilgan |
shu faylni yarating.env.namuna
# Nusxa oling: cp .env.namuna .env # .env ni HECH QACHON git ga qo'ymang. HISOBOT_MAX_BAHO=100 HISOBOT_MIN_BAHO=0 HISOBOT_CHIQISH=./natijalar HISOBOT_QATIY=false # Sirlar (haqiqiy qiymatlarni .env ga yozing) HISOBOT_API_KALIT=
shu faylni yarating.gitignore
.venv/ __pycache__/ *.pyc .pytest_cache/ .mypy_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ # sirlar .env
cat .gitignore echo "--- .env git ga tushadimi? ---" grep -q "^\.env$" .gitignore && echo "yo'q, himoyalangan" || echo "XAVF: .env himoyalanmagan"
.venv/ __pycache__/ *.pyc .pytest_cache/ .mypy_cache/ .coverage htmlcov/ dist/ build/ *.egg-info/ # sirlar .env --- .env git ga tushadimi? --- yo'q, himoyalangan
.env faylini o'qish
Ishlab chiqishda har safar export yozish noqulay. .env fayli shuni yechadi — lekin u faqat
lokal qulaylik, ishlab chiqarishda haqiqiy muhit o'zgaruvchilari ishlatiladi.
shu faylni yaratingsrc/hisobot/muhit.py
"""Ixtiyoriy .env faylini o'qish. Faqat lokal qulaylik uchun.""" import os from pathlib import Path def env_yukla(yol: str | Path = ".env", qayta_yoz: bool = False) -> int: """.env dan o'zgaruvchilarni yuklaydi. Nechta o'zgaruvchi yuklanganini qaytaradi. qayta_yoz=False — allaqachon muhitda bor qiymat ustun turadi (bu to'g'ri xulq: ishlab chiqarishdagi haqiqiy o'zgaruvchi faylga qaraganda kuchliroq bo'lishi kerak). """ p = Path(yol) if not p.exists(): return 0 soni = 0 for xom in p.read_text(encoding="utf-8").splitlines(): qator = xom.strip() if not qator or qator.startswith("#") or "=" not in qator: continue kalit, _, qiymat = qator.partition("=") kalit, qiymat = kalit.strip(), qiymat.strip().strip("'\"") if qayta_yoz or kalit not in os.environ: os.environ[kalit] = qiymat soni += 1 return soni
shu faylni yarating.env
HISOBOT_MAX_BAHO=5 HISOBOT_QATIY=true
.venv/bin/python3 -c " from hisobot.muhit import env_yukla from hisobot.sozlama import Sozlama print('yuklandi:', env_yukla(), 'ta o\'zgaruvchi') print('sozlama:', Sozlama.muhitdan()) " echo "--- haqiqiy muhit o'zgaruvchisi .env dan KUCHLIROQ ---" HISOBOT_MAX_BAHO=200 .venv/bin/python3 -c " from hisobot.muhit import env_yukla from hisobot.sozlama import Sozlama env_yukla() print('sozlama:', Sozlama.muhitdan()) " rm .env
yuklandi: 2 ta o'zgaruvchi
sozlama: Sozlama(max_baho=5, min_baho=0, chiqish_papka=PosixPath('.'), qatiy=True)
--- haqiqiy muhit o'zgaruvchisi .env dan KUCHLIROQ ---
sozlama: Sozlama(max_baho=200, min_baho=0, chiqish_papka=PosixPath('.'), qatiy=True)
Ikkinchi holatda .env da 5 yozilgan bo'lsa ham, haqiqiy muhit o'zgaruvchisi (200) g'olib chiqdi.
Bu to'g'ri tartib: ishlab chiqarishda server bergan qiymat faylga qaraganda muhimroq.
pydantic bilan (muqobil)
Katta loyihalarda pydantic-settings ishlatiladi — u tekshiruvni o'zi qiladi:
from pydantic import Field from pydantic_settings import BaseSettings class Sozlama(BaseSettings): max_baho: int = Field(default=100, gt=0, le=1000) min_baho: int = Field(default=0, ge=0) qatiy: bool = False model_config = {"env_prefix": "HISOBOT_", "env_file": ".env"}
Foydasi: tur aylantirish, chegaralar, .env o'qish — hammasi tayyor. Kamchiligi: qo'shimcha
bog'liqlik. Kichik loyihada yuqoridagi 40 qator yetarli.
Testlar
shu faylni yaratingtests/test_sozlama.py
import pytest from hisobot.sozlama import Sozlama from hisobot.tekshiruv import bahoni_tekshir from hisobot.xatolar import SozlamaXato def test_sukut_qiymatlar(): s = Sozlama() assert s.max_baho == 100 assert s.min_baho == 0 assert s.qatiy is False def test_muhitdan_oqiydi(monkeypatch): monkeypatch.setenv("HISOBOT_MAX_BAHO", "5") monkeypatch.setenv("HISOBOT_QATIY", "ha") s = Sozlama.muhitdan() assert s.max_baho == 5 assert s.qatiy is True @pytest.mark.parametrize("qiymat, kutilgan", [ ("1", True), ("true", True), ("HA", True), ("on", True), ("0", False), ("false", False), ("yo'q", False), ("off", False), ]) def test_mantiqiy_qiymatlar(monkeypatch, qiymat, kutilgan): monkeypatch.setenv("HISOBOT_QATIY", qiymat) assert Sozlama.muhitdan().qatiy is kutilgan def test_notogri_son(monkeypatch): monkeypatch.setenv("HISOBOT_MAX_BAHO", "juda_kop") with pytest.raises(SozlamaXato, match="butun son"): Sozlama.muhitdan() def test_notogri_mantiqiy(monkeypatch): monkeypatch.setenv("HISOBOT_QATIY", "balki") with pytest.raises(SozlamaXato, match="mantiqiy"): Sozlama.muhitdan() def test_ozaro_mos_emas(monkeypatch): monkeypatch.setenv("HISOBOT_MIN_BAHO", "50") monkeypatch.setenv("HISOBOT_MAX_BAHO", "20") with pytest.raises(SozlamaXato, match="kichik bo'lishi"): Sozlama.muhitdan() def test_sozlama_tekshiruvga_tasir_qiladi(): besh = Sozlama(max_baho=5) assert bahoni_tekshir("4", besh) == 4 with pytest.raises(ValueError, match=r"0\.\.5"): # nuqta regex da maxsus belgi bahoni_tekshir("85", besh)
.venv/bin/python3 -m pytest tests/test_sozlama.py -q
.............. [100%]
monkeypatch — pytest fixture i, u muhit o'zgaruvchisini vaqtincha o'zgartiradi va test
tugagach tiklaydi. Global holatni sinashning to'g'ri usuli shu.
.venv/bin/python3 -m pytest -q python3 -m mypy
.................................................................FF. [100%]
=================================== FAILURES ===================================
_____________________________ test_ataylab_notogri _____________________________
def test_ataylab_notogri():
> assert bahoni_tekshir("85") == 58
E AssertionError: assert 85 == 58
E + where 85 = bahoni_tekshir('85')
tests/test_yiqiladi.py:5: AssertionError
___________________________ test_royxat_solishtirish ___________________________
def test_royxat_solishtirish():
kutilgan = ["Aziz", "Bobur", "Dilnoza"]
olingan = ["Aziz", "Bobur", "Elyor"]
> assert olingan == kutilgan
E AssertionError: assert ['Aziz', 'Bobur', 'Elyor'] == ['Aziz', 'Bobur', 'Dilnoza']
E
E At index 2 diff: 'Elyor' != 'Dilnoza'
E Use -v to get more diff
tests/test_yiqiladi.py:11: AssertionError
=========================== short test summary info ============================
FAILED tests/test_yiqiladi.py::test_ataylab_notogri - AssertionError: assert ...
FAILED tests/test_yiqiladi.py::test_royxat_solishtirish - AssertionError: ass...
Success: no issues found in 10 source files
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
| 12-omil | Konfiguratsiya muhit o'zgaruvchilarida, kodda emas |
bool("false") |
Rost! Mantiqiy qiymatni qo'lda tahlil qiling |
| Erta yiqilish | Noto'g'ri sozlama — birinchi soniyada xato |
| O'zaro tekshiruv | Har maydon alohida to'g'ri, lekin birga noto'g'ri bo'lishi mumkin |
| Argument sifatida | Sozlamani global qilmang — testni buzadi |
.env |
Lokal qulaylik; .gitignore ga qo'ying, .env.namuna yozing |
| Ustunlik tartibi | Haqiqiy muhit o'zgaruvchisi .env dan kuchliroq |
| Ochilgan sir | O'chirish yetarli emas — almashtiring |
monkeypatch |
Testda muhitni vaqtincha o'zgartirish |
Talaba ishi
Sozlamagajurnal_darajasi: str = "WARNING"qo'shing va noto'g'ri qiymatni rad eting (faqat DEBUG/INFO/WARNING/ERROR).env_yuklaexport KALIT=qiymatshaklidagi qatorni qanday qayta ishlaydi? Tuzating.HISOBOT_CHIQISHpapkasi mavjud emasliginitekshir()da aniqlang va tushunarli xato bering..envfaylida#bilan boshlanmagan, lekin=yo'q qator bo'lsa nima bo'ladi? Test yozing.
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.