Asosiy qismga o'tish

Konfiguratsiya va sirlar

11-bob 18 kod bloki 2460 so'z

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.

python6 qator
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)"))
Chiqish
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

python57 qator
"""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}")
python9 qator
.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())
"
Chiqish (qaytish kodi 0, 228 ms)
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.

python27 qator
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)
"
Chiqish (qaytish kodi 0, 385 ms)
=== 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:

python3 qator
print('bool("false") =', bool("false"), " <- eng klassik konfiguratsiya xatosi")
print('bool("0")     =', bool("0"))
print('bool("")      =', bool(""))
Chiqish
bool("false") = True  <- eng klassik konfiguratsiya xatosi
bool("0")     = True
bool("")      = False

Sozlamani ishlatamiz

shu faylni yaratingsrc/hisobot/tekshiruv.py

python32 qator
"""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()
python12 qator
.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}')
"
Chiqish (qaytish kodi 0, 101 ms)
  '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.

python12 qator
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)}")
Chiqish
  '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

python10 qator
# 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

matn13 qator
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/

# sirlar
.env
matn3 qator
cat .gitignore
echo "--- .env git ga tushadimi? ---"
grep -q "^\.env$" .gitignore && echo "yo'q, himoyalangan" || echo "XAVF: .env himoyalanmagan"
Chiqish (qaytish kodi 0, 4 ms)
.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

python25 qator
"""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

python2 qator
HISOBOT_MAX_BAHO=5
HISOBOT_QATIY=true
python14 qator
.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
Chiqish (qaytish kodi 0, 276 ms)
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:

python9 qator
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

python55 qator
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)
matn1 qator
.venv/bin/python3 -m pytest tests/test_sozlama.py -q
Chiqish (qaytish kodi 0, 1339 ms)
..............                                                           [100%]

monkeypatch — pytest fixture i, u muhit o'zgaruvchisini vaqtincha o'zgartiradi va test tugagach tiklaydi. Global holatni sinashning to'g'ri usuli shu.

matn2 qator
.venv/bin/python3 -m pytest -q
python3 -m mypy
Chiqish (qaytish kodi 0, 7360 ms)
.................................................................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

  1. Sozlama ga jurnal_darajasi: str = "WARNING" qo'shing va noto'g'ri qiymatni rad eting (faqat DEBUG/INFO/WARNING/ERROR).
  2. env_yukla export KALIT=qiymat shaklidagi qatorni qanday qayta ishlaydi? Tuzating.
  3. HISOBOT_CHIQISH papkasi mavjud emasligini tekshir() da aniqlang va tushunarli xato bering.
  4. .env faylida # 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.

Telegramda yozish Narxlar