udzialy-bot 2.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.
Files changed (62) hide show
  1. udzialy_bot-2.1.0/LICENSE +13 -0
  2. udzialy_bot-2.1.0/PKG-INFO +169 -0
  3. udzialy_bot-2.1.0/README.md +135 -0
  4. udzialy_bot-2.1.0/bot/__init__.py +7 -0
  5. udzialy_bot-2.1.0/bot/__main__.py +3 -0
  6. udzialy_bot-2.1.0/bot/config.py +191 -0
  7. udzialy_bot-2.1.0/bot/keyboards/__init__.py +27 -0
  8. udzialy_bot-2.1.0/bot/keyboards/inline.py +231 -0
  9. udzialy_bot-2.1.0/bot/keyboards/reply.py +47 -0
  10. udzialy_bot-2.1.0/bot/main.py +174 -0
  11. udzialy_bot-2.1.0/bot/middlewares/__init__.py +16 -0
  12. udzialy_bot-2.1.0/bot/middlewares/throttle.py +81 -0
  13. udzialy_bot-2.1.0/bot/routers/__init__.py +25 -0
  14. udzialy_bot-2.1.0/bot/routers/filters.py +347 -0
  15. udzialy_bot-2.1.0/bot/routers/results.py +300 -0
  16. udzialy_bot-2.1.0/bot/routers/saved.py +226 -0
  17. udzialy_bot-2.1.0/bot/routers/search.py +382 -0
  18. udzialy_bot-2.1.0/bot/routers/settings.py +130 -0
  19. udzialy_bot-2.1.0/detector/__init__.py +26 -0
  20. udzialy_bot-2.1.0/detector/filters.py +172 -0
  21. udzialy_bot-2.1.0/detector/keywords.py +165 -0
  22. udzialy_bot-2.1.0/detector/llm_analyzer.py +773 -0
  23. udzialy_bot-2.1.0/detector/scorer.py +362 -0
  24. udzialy_bot-2.1.0/geo/__init__.py +12 -0
  25. udzialy_bot-2.1.0/geo/cities.py +213 -0
  26. udzialy_bot-2.1.0/geo/distance.py +136 -0
  27. udzialy_bot-2.1.0/launcher.py +1076 -0
  28. udzialy_bot-2.1.0/pyproject.toml +58 -0
  29. udzialy_bot-2.1.0/scraper/__init__.py +34 -0
  30. udzialy_bot-2.1.0/scraper/base.py +117 -0
  31. udzialy_bot-2.1.0/scraper/manager.py +281 -0
  32. udzialy_bot-2.1.0/scraper/portals/__init__.py +36 -0
  33. udzialy_bot-2.1.0/scraper/portals/allegro.py +297 -0
  34. udzialy_bot-2.1.0/scraper/portals/domiporta.py +188 -0
  35. udzialy_bot-2.1.0/scraper/portals/gratka.py +175 -0
  36. udzialy_bot-2.1.0/scraper/portals/morizon.py +178 -0
  37. udzialy_bot-2.1.0/scraper/portals/nieruchomosci_online.py +220 -0
  38. udzialy_bot-2.1.0/scraper/portals/olx.py +265 -0
  39. udzialy_bot-2.1.0/scraper/portals/otodom.py +258 -0
  40. udzialy_bot-2.1.0/scraper/portals/szybko.py +217 -0
  41. udzialy_bot-2.1.0/scraper/portals/trojmiasto.py +38 -0
  42. udzialy_bot-2.1.0/scraper/stealth.py +372 -0
  43. udzialy_bot-2.1.0/scraper/tor_manager.py +306 -0
  44. udzialy_bot-2.1.0/scraper/utils/__init__.py +6 -0
  45. udzialy_bot-2.1.0/scraper/utils/headers.py +82 -0
  46. udzialy_bot-2.1.0/scraper/utils/retry.py +115 -0
  47. udzialy_bot-2.1.0/setup.cfg +4 -0
  48. udzialy_bot-2.1.0/storage/__init__.py +12 -0
  49. udzialy_bot-2.1.0/storage/database.py +274 -0
  50. udzialy_bot-2.1.0/storage/models.py +177 -0
  51. udzialy_bot-2.1.0/storage/queries.py +487 -0
  52. udzialy_bot-2.1.0/tests/test_distance.py +140 -0
  53. udzialy_bot-2.1.0/tests/test_llm_analyzer.py +667 -0
  54. udzialy_bot-2.1.0/tests/test_pipeline_live.py +121 -0
  55. udzialy_bot-2.1.0/tests/test_scorer.py +228 -0
  56. udzialy_bot-2.1.0/udzialy_bot.egg-info/PKG-INFO +169 -0
  57. udzialy_bot-2.1.0/udzialy_bot.egg-info/SOURCES.txt +60 -0
  58. udzialy_bot-2.1.0/udzialy_bot.egg-info/dependency_links.txt +1 -0
  59. udzialy_bot-2.1.0/udzialy_bot.egg-info/entry_points.txt +5 -0
  60. udzialy_bot-2.1.0/udzialy_bot.egg-info/requires.txt +14 -0
  61. udzialy_bot-2.1.0/udzialy_bot.egg-info/top_level.txt +7 -0
  62. udzialy_bot-2.1.0/udzialy_cli.py +245 -0
@@ -0,0 +1,13 @@
1
+ Udzialy Bot - Wyszukiwarka Udzialow
2
+ Copyright (c) 2024-2026 QA10 sp. z o.o.
3
+ Wszelkie prawa zastrzezone / All rights reserved.
4
+
5
+ To oprogramowanie jest licencjonowane wylacznie do uzytku osobistego.
6
+ Redystrybucja, modyfikacja i wykorzystanie komercyjne bez pisemnej
7
+ zgody QA10 sp. z o.o. jest zabronione.
8
+
9
+ This software is licensed for personal use only.
10
+ Redistribution, modification and commercial use without written
11
+ permission from QA10 sp. z o.o. is prohibited.
12
+
13
+ Kontakt / Contact: kontakt@qa10.pl
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.4
2
+ Name: udzialy-bot
3
+ Version: 2.1.0
4
+ Summary: Bot do wyszukiwania ogłoszeń sprzedaży udziałów w nieruchomościach
5
+ Author-email: "QA10 sp. z o.o." <hello@qa10.io>
6
+ License: MIT
7
+ Keywords: telegram,bot,real-estate,scraping,poland,udzialy
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: End Users/Desktop
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: Microsoft :: Windows
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Communications :: Chat
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: aiogram>=3.10
20
+ Requires-Dist: aiosqlite>=0.19.0
21
+ Requires-Dist: httpx[http2,socks]>=0.27.0
22
+ Requires-Dist: selectolax>=0.3.21
23
+ Requires-Dist: curl_cffi>=0.7.0
24
+ Requires-Dist: primp>=0.6.0
25
+ Requires-Dist: nodriver>=0.35
26
+ Requires-Dist: patchright>=1.43
27
+ Requires-Dist: stem>=1.8.2
28
+ Requires-Dist: PySocks>=1.7.1
29
+ Requires-Dist: beautifulsoup4>=4.12.0
30
+ Requires-Dist: pydantic>=2.7.0
31
+ Requires-Dist: pydantic-settings>=2.3.0
32
+ Requires-Dist: pyyaml>=6.0.1
33
+ Dynamic: license-file
34
+
35
+ # 🏠 Udziały Bot
36
+
37
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
38
+ [![aiogram 3.x](https://img.shields.io/badge/aiogram-3.x-green.svg)](https://docs.aiogram.dev/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
40
+ [![Platform: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]()
41
+
42
+ **Telegram bot do wyszukiwania ogłoszeń sprzedaży udziałów w nieruchomościach** na 9 polskich portalach ogłoszeniowych.
43
+
44
+ Bot automatycznie przeszukuje portale nieruchomości w poszukiwaniu ofert sprzedaży udziałów (ułamkowych części własności) i prezentuje wyniki bezpośrednio w Telegramie.
45
+
46
+ ---
47
+
48
+ ## ✨ Funkcje
49
+
50
+ - 🔍 Przeszukiwanie 9 portali nieruchomości jednocześnie
51
+ - 🗺️ Filtrowanie po województwie, mieście, promieniu i cenie
52
+ - 📄 Paginacja wyników z klawiaturą inline
53
+ - 💾 Zapisywanie interesujących ogłoszeń
54
+ - 🛡️ Rotacja proxy przez Tor dla anonimowości
55
+ - ⚡ Asynchroniczne scrapowanie (httpx, curl_cffi, nodriver)
56
+ - 🧠 FSM do konfiguracji filtrów krok po kroku
57
+ - 🚦 Anti-flood middleware
58
+
59
+ ## 📋 Wymagania
60
+
61
+ - Python 3.11 lub nowszy
62
+ - Windows 10/11 (skrypty .bat)
63
+ - Konto Telegram + token bota z [@BotFather](https://t.me/BotFather)
64
+
65
+ ## 🚀 Instalacja (Windows)
66
+
67
+ ### Automatyczna (zalecana)
68
+
69
+ 1. Sklonuj repozytorium:
70
+ ```cmd
71
+ git clone https://github.com/your-user/udzialy-bot.git
72
+ cd udzialy-bot
73
+ ```
74
+
75
+ 2. Uruchom instalator:
76
+ ```cmd
77
+ install.bat
78
+ ```
79
+ Skrypt automatycznie:
80
+ - Sprawdzi wersję Pythona
81
+ - Utworzy środowisko wirtualne (venv)
82
+ - Zainstaluje zależności
83
+ - Pobierze Tor Expert Bundle
84
+ - Zainstaluje przeglądarki Playwright
85
+
86
+ 3. Skonfiguruj bota — edytuj `config.yaml`:
87
+ ```yaml
88
+ telegram:
89
+ token: "YOUR_BOT_TOKEN_HERE"
90
+ owner_id: 123456789
91
+ ```
92
+
93
+ ### Ręczna
94
+
95
+ ```cmd
96
+ python -m venv .venv
97
+ .venv\Scripts\activate
98
+ pip install -r requirements.txt
99
+ playwright install chromium
100
+ ```
101
+
102
+ ## ▶️ Uruchomienie
103
+
104
+ ```cmd
105
+ start_bot.bat
106
+ ```
107
+
108
+ Lub ręcznie:
109
+ ```cmd
110
+ .venv\Scripts\activate
111
+ python -m bot.main
112
+ ```
113
+
114
+ ## 🎮 Komendy bota
115
+
116
+ | Komenda | Opis |
117
+ |---------|------|
118
+ | `/start` | Powitanie i menu główne |
119
+ | `/search` | Rozpocznij nowe wyszukiwanie |
120
+ | `/filters` | Ustaw filtry (województwo, miasto, cena) |
121
+ | `/saved` | Pokaż zapisane ogłoszenia |
122
+ | `/settings` | Ustawienia użytkownika |
123
+ | `/help` | Pomoc |
124
+
125
+ ## 📁 Struktura projektu
126
+
127
+ ```
128
+ udzialy-bot/
129
+ ├── bot/
130
+ │ ├── __init__.py
131
+ │ ├── main.py # Entry point, dispatcher
132
+ │ ├── config.py # Pydantic Settings
133
+ │ ├── routers/
134
+ │ │ ├── __init__.py # Router registration
135
+ │ │ ├── search.py # /search command
136
+ │ │ ├── results.py # Pagination
137
+ │ │ ├── filters.py # FSM filters
138
+ │ │ ├── saved.py # Saved listings
139
+ │ │ └── settings.py # User settings
140
+ │ ├── keyboards/
141
+ │ │ ├── __init__.py
142
+ │ │ ├── inline.py # Inline keyboards
143
+ │ │ └── reply.py # Reply keyboards
144
+ │ └── middlewares/
145
+ │ ├── __init__.py
146
+ │ └── throttle.py # Anti-flood
147
+ ├── config.yaml # Configuration
148
+ ├── requirements.txt # Dependencies
149
+ ├── pyproject.toml # Project metadata
150
+ ├── install.bat # Windows installer
151
+ ├── start_bot.bat # Windows launcher
152
+ ├── .gitignore
153
+ └── README.md
154
+ ```
155
+
156
+ ## ⚙️ Konfiguracja
157
+
158
+ Plik `config.yaml` zawiera pełną konfigurację bota. Wartości można nadpisać zmiennymi środowiskowymi z prefiksem `UDZIALY_`.
159
+
160
+ ## 🔒 Bezpieczeństwo
161
+
162
+ - Bot działa w trybie single-user (tylko owner_id)
163
+ - Tor zapewnia anonimowość przy scrapowaniu
164
+ - Rotacja User-Agent i fingerprint
165
+ - Rate limiting na portalach
166
+
167
+ ## 📄 Licencja
168
+
169
+ MIT License — do użytku osobistego.
@@ -0,0 +1,135 @@
1
+ # 🏠 Udziały Bot
2
+
3
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
4
+ [![aiogram 3.x](https://img.shields.io/badge/aiogram-3.x-green.svg)](https://docs.aiogram.dev/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+ [![Platform: Windows](https://img.shields.io/badge/platform-Windows-lightgrey.svg)]()
7
+
8
+ **Telegram bot do wyszukiwania ogłoszeń sprzedaży udziałów w nieruchomościach** na 9 polskich portalach ogłoszeniowych.
9
+
10
+ Bot automatycznie przeszukuje portale nieruchomości w poszukiwaniu ofert sprzedaży udziałów (ułamkowych części własności) i prezentuje wyniki bezpośrednio w Telegramie.
11
+
12
+ ---
13
+
14
+ ## ✨ Funkcje
15
+
16
+ - 🔍 Przeszukiwanie 9 portali nieruchomości jednocześnie
17
+ - 🗺️ Filtrowanie po województwie, mieście, promieniu i cenie
18
+ - 📄 Paginacja wyników z klawiaturą inline
19
+ - 💾 Zapisywanie interesujących ogłoszeń
20
+ - 🛡️ Rotacja proxy przez Tor dla anonimowości
21
+ - ⚡ Asynchroniczne scrapowanie (httpx, curl_cffi, nodriver)
22
+ - 🧠 FSM do konfiguracji filtrów krok po kroku
23
+ - 🚦 Anti-flood middleware
24
+
25
+ ## 📋 Wymagania
26
+
27
+ - Python 3.11 lub nowszy
28
+ - Windows 10/11 (skrypty .bat)
29
+ - Konto Telegram + token bota z [@BotFather](https://t.me/BotFather)
30
+
31
+ ## 🚀 Instalacja (Windows)
32
+
33
+ ### Automatyczna (zalecana)
34
+
35
+ 1. Sklonuj repozytorium:
36
+ ```cmd
37
+ git clone https://github.com/your-user/udzialy-bot.git
38
+ cd udzialy-bot
39
+ ```
40
+
41
+ 2. Uruchom instalator:
42
+ ```cmd
43
+ install.bat
44
+ ```
45
+ Skrypt automatycznie:
46
+ - Sprawdzi wersję Pythona
47
+ - Utworzy środowisko wirtualne (venv)
48
+ - Zainstaluje zależności
49
+ - Pobierze Tor Expert Bundle
50
+ - Zainstaluje przeglądarki Playwright
51
+
52
+ 3. Skonfiguruj bota — edytuj `config.yaml`:
53
+ ```yaml
54
+ telegram:
55
+ token: "YOUR_BOT_TOKEN_HERE"
56
+ owner_id: 123456789
57
+ ```
58
+
59
+ ### Ręczna
60
+
61
+ ```cmd
62
+ python -m venv .venv
63
+ .venv\Scripts\activate
64
+ pip install -r requirements.txt
65
+ playwright install chromium
66
+ ```
67
+
68
+ ## ▶️ Uruchomienie
69
+
70
+ ```cmd
71
+ start_bot.bat
72
+ ```
73
+
74
+ Lub ręcznie:
75
+ ```cmd
76
+ .venv\Scripts\activate
77
+ python -m bot.main
78
+ ```
79
+
80
+ ## 🎮 Komendy bota
81
+
82
+ | Komenda | Opis |
83
+ |---------|------|
84
+ | `/start` | Powitanie i menu główne |
85
+ | `/search` | Rozpocznij nowe wyszukiwanie |
86
+ | `/filters` | Ustaw filtry (województwo, miasto, cena) |
87
+ | `/saved` | Pokaż zapisane ogłoszenia |
88
+ | `/settings` | Ustawienia użytkownika |
89
+ | `/help` | Pomoc |
90
+
91
+ ## 📁 Struktura projektu
92
+
93
+ ```
94
+ udzialy-bot/
95
+ ├── bot/
96
+ │ ├── __init__.py
97
+ │ ├── main.py # Entry point, dispatcher
98
+ │ ├── config.py # Pydantic Settings
99
+ │ ├── routers/
100
+ │ │ ├── __init__.py # Router registration
101
+ │ │ ├── search.py # /search command
102
+ │ │ ├── results.py # Pagination
103
+ │ │ ├── filters.py # FSM filters
104
+ │ │ ├── saved.py # Saved listings
105
+ │ │ └── settings.py # User settings
106
+ │ ├── keyboards/
107
+ │ │ ├── __init__.py
108
+ │ │ ├── inline.py # Inline keyboards
109
+ │ │ └── reply.py # Reply keyboards
110
+ │ └── middlewares/
111
+ │ ├── __init__.py
112
+ │ └── throttle.py # Anti-flood
113
+ ├── config.yaml # Configuration
114
+ ├── requirements.txt # Dependencies
115
+ ├── pyproject.toml # Project metadata
116
+ ├── install.bat # Windows installer
117
+ ├── start_bot.bat # Windows launcher
118
+ ├── .gitignore
119
+ └── README.md
120
+ ```
121
+
122
+ ## ⚙️ Konfiguracja
123
+
124
+ Plik `config.yaml` zawiera pełną konfigurację bota. Wartości można nadpisać zmiennymi środowiskowymi z prefiksem `UDZIALY_`.
125
+
126
+ ## 🔒 Bezpieczeństwo
127
+
128
+ - Bot działa w trybie single-user (tylko owner_id)
129
+ - Tor zapewnia anonimowość przy scrapowaniu
130
+ - Rotacja User-Agent i fingerprint
131
+ - Rate limiting na portalach
132
+
133
+ ## 📄 Licencja
134
+
135
+ MIT License — do użytku osobistego.
@@ -0,0 +1,7 @@
1
+ """
2
+ Udziały Bot — Telegram bot for scraping Polish real estate portals
3
+ for property share (udział w nieruchomości) listings.
4
+ """
5
+
6
+ __version__ = "0.1.0"
7
+ __app_name__ = "udzialy-bot"
@@ -0,0 +1,3 @@
1
+ from bot.main import main
2
+
3
+ main()
@@ -0,0 +1,191 @@
1
+ """
2
+ Bot configuration — loads config.yaml relative to project root.
3
+
4
+ Exposes a Settings dataclass with: telegram_token, owner_id, portals dict,
5
+ tor config, scraping timeouts. Uses pydantic-settings with YAML source.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+
13
+ import yaml
14
+ from pydantic import BaseModel, Field
15
+ from pydantic_settings import BaseSettings, SettingsConfigDict
16
+
17
+
18
+ # --- Project root detection ---
19
+
20
+ def _find_project_root() -> Path:
21
+ """Find project root by looking for config.yaml upward from this file."""
22
+ current = Path(__file__).resolve().parent # bot/
23
+ for parent in [current.parent, current.parent.parent, Path.cwd()]:
24
+ if (parent / "config.yaml").exists():
25
+ return parent
26
+ # Fallback: assume CWD is project root
27
+ return Path.cwd()
28
+
29
+
30
+ PROJECT_ROOT = _find_project_root()
31
+
32
+
33
+ # --- Sub-models ---
34
+
35
+ class TelegramConfig(BaseModel):
36
+ """Telegram bot settings."""
37
+ token: str = "YOUR_BOT_TOKEN_HERE"
38
+ owner_id: int = 0
39
+
40
+
41
+ class PortalEntry(BaseModel):
42
+ """Single portal configuration."""
43
+ enabled: bool = True
44
+ base_url: str = ""
45
+
46
+
47
+ class PortalsConfig(BaseModel):
48
+ """All portal switches — dynamic dict of portal entries."""
49
+ model_config = {"extra": "allow"}
50
+
51
+ otodom: PortalEntry = PortalEntry()
52
+ olx: PortalEntry = PortalEntry()
53
+ gratka: PortalEntry = PortalEntry()
54
+ morizon: PortalEntry = PortalEntry()
55
+ nieruchomosci_online: PortalEntry = PortalEntry()
56
+ domiporta: PortalEntry = PortalEntry()
57
+ lento: PortalEntry = PortalEntry()
58
+ gethome: PortalEntry = PortalEntry()
59
+ ogloszenia24: PortalEntry = PortalEntry(enabled=False)
60
+
61
+ def enabled_portals(self) -> list[str]:
62
+ """Return list of enabled portal names."""
63
+ result = []
64
+ for name in self.model_fields:
65
+ entry = getattr(self, name, None)
66
+ if isinstance(entry, PortalEntry) and entry.enabled:
67
+ result.append(name)
68
+ return result
69
+
70
+ def as_dict(self) -> Dict[str, Dict[str, Any]]:
71
+ """Return portals as plain dict."""
72
+ result = {}
73
+ for name in self.model_fields:
74
+ entry = getattr(self, name, None)
75
+ if isinstance(entry, PortalEntry):
76
+ result[name] = {"enabled": entry.enabled, "base_url": entry.base_url}
77
+ return result
78
+
79
+
80
+ class ScrapingConfig(BaseModel):
81
+ """Scraping behavior settings."""
82
+ timeout: int = 30
83
+ max_concurrent: int = 3
84
+ retry_count: int = 2
85
+ retry_delay: int = 5
86
+ delay_between: int = 2
87
+ user_agent_rotate: bool = True
88
+
89
+
90
+ class TorConfig(BaseModel):
91
+ """Tor proxy settings."""
92
+ enabled: bool = True
93
+ socks_port: int = 9050
94
+ control_port: int = 9051
95
+ control_password: str = "udzialy2026"
96
+ circuit_rotate_interval: int = 300
97
+ binary_path: str = "tor/tor.exe"
98
+
99
+ @property
100
+ def proxy_url(self) -> str:
101
+ """SOCKS5 proxy URL for httpx."""
102
+ return f"socks5://127.0.0.1:{self.socks_port}"
103
+
104
+
105
+ class DatabaseConfig(BaseModel):
106
+ """Database settings."""
107
+ path: str = "data/udzialy.db"
108
+
109
+
110
+ class LLMConfig(BaseModel):
111
+ """LLM analysis settings (optional)."""
112
+ enabled: bool = False
113
+ provider: str = "openai" # openai, anthropic, local
114
+ api_key: str = ""
115
+ model: str = "gpt-4o-mini"
116
+ base_url: str = "https://api.openai.com/v1"
117
+ max_concurrent: int = 5
118
+ timeout: int = 15
119
+
120
+
121
+ class LoggingConfig(BaseModel):
122
+ """Logging settings."""
123
+ level: str = "INFO"
124
+ file: str = "data/bot.log"
125
+
126
+
127
+ # --- Main Settings ---
128
+
129
+ class Settings(BaseSettings):
130
+ """
131
+ Application settings loaded from config.yaml + environment variables.
132
+
133
+ Environment variables override YAML values with prefix UDZIALY_.
134
+ Example: UDZIALY_TELEGRAM__TOKEN=xxx overrides telegram.token.
135
+ """
136
+
137
+ model_config = SettingsConfigDict(
138
+ env_prefix="UDZIALY_",
139
+ env_nested_delimiter="__",
140
+ case_sensitive=False,
141
+ )
142
+
143
+ telegram: TelegramConfig = TelegramConfig()
144
+ portals: PortalsConfig = PortalsConfig()
145
+ scraping: ScrapingConfig = ScrapingConfig()
146
+ tor: TorConfig = TorConfig()
147
+ database: DatabaseConfig = DatabaseConfig()
148
+ llm: LLMConfig = LLMConfig()
149
+ logging: LoggingConfig = LoggingConfig()
150
+
151
+ # Convenience properties
152
+ @property
153
+ def telegram_token(self) -> str:
154
+ return self.telegram.token
155
+
156
+ @property
157
+ def owner_id(self) -> int:
158
+ return self.telegram.owner_id
159
+
160
+ @classmethod
161
+ def from_yaml(cls, path: str | Path | None = None) -> "Settings":
162
+ """Load settings from YAML file, then apply env var overrides."""
163
+ if path is None:
164
+ path = PROJECT_ROOT / "config.yaml"
165
+ config_path = Path(path)
166
+ yaml_data: dict[str, Any] = {}
167
+
168
+ if config_path.exists():
169
+ with open(config_path, "r", encoding="utf-8") as f:
170
+ yaml_data = yaml.safe_load(f) or {}
171
+
172
+ return cls(**yaml_data)
173
+
174
+
175
+ # --- Singleton ---
176
+
177
+ _settings: Settings | None = None
178
+
179
+
180
+ def get_settings(config_path: str | Path | None = None) -> Settings:
181
+ """Get or create settings singleton."""
182
+ global _settings
183
+ if _settings is None:
184
+ _settings = Settings.from_yaml(config_path)
185
+ return _settings
186
+
187
+
188
+ def reset_settings() -> None:
189
+ """Reset singleton (for testing)."""
190
+ global _settings
191
+ _settings = None
@@ -0,0 +1,27 @@
1
+ """
2
+ Keyboards package — inline and reply keyboard builders.
3
+ """
4
+
5
+ from bot.keyboards.inline import (
6
+ build_results_keyboard,
7
+ build_search_progress_keyboard,
8
+ build_listing_detail_keyboard,
9
+ build_voivodeship_keyboard,
10
+ build_radius_keyboard,
11
+ build_filter_summary_keyboard,
12
+ build_saved_keyboard,
13
+ build_settings_keyboard,
14
+ )
15
+ from bot.keyboards.reply import main_menu_keyboard
16
+
17
+ __all__ = [
18
+ "build_results_keyboard",
19
+ "build_search_progress_keyboard",
20
+ "build_listing_detail_keyboard",
21
+ "build_voivodeship_keyboard",
22
+ "build_radius_keyboard",
23
+ "build_filter_summary_keyboard",
24
+ "build_saved_keyboard",
25
+ "build_settings_keyboard",
26
+ "main_menu_keyboard",
27
+ ]