Metadata-Version: 2.4
Name: weatherender
Version: 2.0.1
Summary: Production-grade weather intelligence for skiers
Author-email: Alexey Lyapin <lehacomp16@gmail.com>
License: SSCI Custom License v1.2
        © 2026 Alexey Lyapin (Alexey Lyapin)
        Contact: lehacomp16@gmail.com
        Project: Weatherender / SSCI — Snow Surface Condition Index
        
        This license applies to all files, code, algorithms, formulas, models, data,
        API endpoints, documentation, and any other content contained in this repository
        and its PyPI package, including all future versions and updates.
        
        ---
        
        1. GRANT OF PERMISSION (FOR NON-COMMERCIAL USE ONLY)
           Permission is granted to any person or organization to download, view, copy,
           modify, and use this work for non-commercial purposes only, provided that:
           - all copies retain this license,
           - all modifications are clearly documented,
           - the original author is credited as required in Section 2.
        
        2. MANDATORY ATTRIBUTION
           Any use of this work (including modified versions) must include clear and
           visible attribution to the original author:
           "SSCI / Weatherender by Alexey Lyapin"
           with a functioning link to the original repository:
           https://github.com/Weatherender-foundation/Weatherender
        
        3. DERIVATIVE WORKS
           If you modify or build upon this work, you must:
           - clearly state that the work is based on the SSCI model,
           - include this license in full with your derivative,
           - not use the SSCI name or branding to promote your derivative.
        
        4. COMMERCIAL USE — STRICTLY PROHIBITED
           Commercial use of this work, or any derivative of it, is strictly forbidden
           without explicit written permission from the author.
        
           "Commercial use" means any activity that generates revenue, profit,
           or commercial advantage, including but not limited to:
           - selling, renting, or sublicensing the software,
           - using it in a paid service, SaaS, or subscription platform,
           - integrating it into a product that is sold or monetized,
           - using it for internal business operations that support revenue generation,
           - using it in advertising, marketing, or promotional materials,
           - using it as part of a paid API or backend service,
           - using it in a mobile or web application that charges users or displays paid ads.
        
           Without explicit written permission from the author, any such use is illegal
           and constitutes a violation of this license.
        
        5. NO REMOVAL OF ATTRIBUTION
           You may not remove, alter, hide, or obscure the author's name, copyright notice,
           license text, or any reference to the SSCI / Weatherender project.
        
        6. NO SUBLICENSING
           You may not grant any commercial rights to third parties. Only the author
           may issue commercial licenses.
        
        7. REVERSE ENGINEERING PROHIBITED
           You may not reverse engineer, extract, or replicate the underlying algorithm
           or model for the purpose of creating a competing commercial product.
        
        8. AI AND MODEL TRAINING RESTRICTIONS
           This work may not be used to train AI models, machine learning systems,
           or automated code generation tools without explicit written permission.
        
        9. NO WARRANTY
           This work is provided "as is" without warranty of any kind. The author is not
           liable for any damages arising from its use.
        
        10. CONTACT FOR COMMERCIAL LICENSING
            For commercial licensing inquiries, contact:
            Alexey Lyapin — lehacomp16@gmail.com
        
            Commercial licenses may be granted at the author's sole discretion.
        
Project-URL: Homepage, https://github.com/Weatherender-foundation/Weatherender
Project-URL: Repository, https://github.com/Weatherender-foundation/Weatherender
Project-URL: Documentation, https://github.com/Weatherender-foundation/Weatherender/blob/main/docs/API.md
Project-URL: Issues, https://github.com/Weatherender-foundation/Weatherender/issues
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Flask==3.0.3
Requires-Dist: gunicorn==22.0.0
Requires-Dist: gevent==26.8.0
Requires-Dist: psycogreen==1.0.2
Requires-Dist: flask-limiter==4.1.1
Requires-Dist: flask-swagger-ui==5.32.9
Requires-Dist: flask-talisman==1.1.0
Requires-Dist: prometheus-flask-exporter==0.23.2
Requires-Dist: apispec==6.10.0
Requires-Dist: apispec-webframeworks==1.2.0
Requires-Dist: fastapi==0.141.1
Requires-Dist: uvicorn==0.52.4
Requires-Dist: slowapi==0.1.10
Requires-Dist: requests==2.32.3
Requires-Dist: httpx==0.28.1
Requires-Dist: a2wsgi==1.10.10
Requires-Dist: twine==7.0.0
Requires-Dist: build==1.6.0
Requires-Dist: SQLAlchemy==2.0.35
Requires-Dist: psycopg2-binary==2.9.12
Requires-Dist: alembic==1.13.3
Requires-Dist: asyncpg==0.31.0
Requires-Dist: redis==8.1.0
Requires-Dist: types-redis==4.6.0.20241004
Requires-Dist: marshmallow==3.22.0
Requires-Dist: python-dotenv==1.0.1
Requires-Dist: python-dateutil==2.9.0
Requires-Dist: types-python-dateutil==2.9.0.20260716
Requires-Dist: apscheduler==3.11.3
Requires-Dist: pytest==9.1.1
Requires-Dist: pytest_asyncio==1.4.0
Requires-Dist: pytest-cov==7.1.0
Requires-Dist: pre-commit==4.6.0
Requires-Dist: mypy==2.3.0
Requires-Dist: types-requests==2.33.0.20260712
Requires-Dist: respx==0.23.1
Requires-Dist: tenacity==9.1.4
Dynamic: license-file

<img src="https://avatars.githubusercontent.com/u/323290598?s=100&v=4" align="left" width="70" style="margin-right: 15px;">

# Weatherender

Production-grade weather application with a Flask web interface, a CLI tool, and a high-performance async FastAPI v2 API, built as a portfolio project demonstrating real-world engineering practices, has been developed by Alexey Lyapin.

[![CI](https://github.com/Weatherender-foundation/Weatherender/actions/workflows/ci.yml/badge.svg)](https://github.com/Weatherender-foundation/Weatherender/actions)
[![Release](https://img.shields.io/github/v/release/Weatherender-foundation/Weatherender)](https://github.com/Weatherender-foundation/Weatherender/releases/latest)
[![PyPI](https://img.shields.io/pypi/v/weatherender)](https://pypi.org/project/weatherender/)
[![GHCR](https://img.shields.io/badge/GHCR-weatherender--api-blue)](https://github.com/Weatherender-foundation/Weatherender/pkgs/container/weatherender-api)
[![Codecov](https://codecov.io/github/Weatherender-foundation/Weatherender/graph/badge.svg?token=VIAZVWQ81B)](https://codecov.io/github/Weatherender-foundation/Weatherender)
<br/>
![Python](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=306998)
![Flask](https://img.shields.io/badge/Flask-Framework-000000?logo=flask&logoColor=white)
![Docker](https://img.shields.io/badge/Docker-Container-2496ED?logo=docker&logoColor=white)
![License](https://img.shields.io/badge/SSCI-Custom_License-green)

## Live Demo

> The application is currently in a fully stable, containerized, and production-grade state. It will remain active and autonomously maintained in the cloud.

**[weather-7icc.onrender.com](https://weather-7icc.onrender.com)**

> **Infrastructure Note:** Hosted on the Render free tier. To bypass the default 15-minute spin-down restriction, the application is kept active via a dedicated background automated worker ([UptimeRobot](https://uptimerobot.com/)) targeting the lightweight database-free `/api/ping` endpoint every 10 minutes.

### Availability and Known Limitations

- **Cold Starts:** Despite the cron-ping system, occasional "cold starts" (30–50s delays) may still occur due to Render's internal container recycling or rare service interruptions.
- **Database Pauses:** The production storage operates on a free Supabase instance. If the database receives absolutely no client traffic for 7+ consecutive days, Supabase will automatically pause the project. If this occurs, feel free to open an issue to request a manual wake-up.

Stack in production: Render (app) + Supabase (PostgreSQL) + Upstash (Redis).

## Install

### Python package (CLI)

```bash
pip install weatherender
weatherender
```

Requires Python 3.13+ and a configured `.env` (at minimum `WEATHER_API_KEY`).

### Docker image (API)

```bash
docker pull ghcr.io/weatherender-foundation/weatherender-api:latest
```

For the full local stack (web + api + postgres + redis) use Docker Compose below.

## Features

- 🌦 Current weather + 3-day forecast via [WeatherAPI](https://www.weatherapi.com/)
- ❄️ Advanced Snow Surface Condition Index(SSCI): a unique algorithmic snow condition and quality detection system (featuring statuses like *Dry champagne powder!*, *Ice crust*, *Spring slush*, *Wind slab*, etc.). The analysis evaluates diurnal temperature cycles, wind speed, snow density, and 24-hour precipitation metrics.
- 🖥 Web interface (Flask) and CLI tool, sharing a common service/model layer
- 📍 Automatic city detection by IP (with fallback chain: ip-api.com → ipinfo.io)
- 🗄 PostgreSQL persistence via SQLAlchemy + Alembic migrations
- 🧹 Automated DB cleanup: background storage rotation powered by `APScheduler` (running weekly with a file-lock mechanism to prevent duplicate worker triggers) to safely stay within DB limits
- ✅ Input validation with Marshmallow (sync) and Pydantic v2 (async)
- 🛡️ Resilience & retries (`tenacity`): exponential backoff wrapper for upstream WeatherAPI calls (synchronous for v1 Flask `requests`, asynchronous non-blocking for v2 FastAPI `httpx`)
- 🚦 Rate limiting & protection: granular per-worker rate limiting (`flask-limiter` for v1, `slowapi` for v2), confirmed experimentally via `scripts/check_limit.sh` (400 sequential requests, graceful `429 Too Many Requests` degradation beyond the quota)
- 🐳 Fully containerized with Docker Compose
- 🐬 Docker image on GHCR: [`ghcr.io/weatherender-foundation/weatherender-api`](https://github.com/Weatherender-foundation/Weatherender/pkgs/container/weatherender-api)
- 🐍 Published on PyPI: [`pip install weatherender`](https://pypi.org/project/weatherender/)
- 🔄 CI/CD via GitHub Actions (build, migrate, health check, image publish)
- 🧪 142+ automated tests (pytest): unit, mocked service, Flask & FastAPIroute, and real PostgreSQL integration tests
- 🔌 JSON REST API (`/api/weather` and `/api/v2/weather`) with interactive Swagger/OpenAPI docs
- ⚡️ Redis caching for WeatherAPI responses (TTL-based, graceful fallback on Redis unavailability)
- 📊 Prometheus metrics endpoint (`/metrics`) for observability
- ❤️ Readiness health check (`/health`) with Docker/Compose integration
- 🔒 Security hardening: secure headers (Talisman), request size limits, User-Agent validation
- 📝 Structured JSON logging
- 🚀 Load-tested with [k6](https://k6.io/) (smoke, load, stress, spike) — see [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md)
- ☁️ Production cloud deployment: hosted on Render, integrated with Supabase (PostgreSQL) and Upstash (Redis), featuring an automated heartbeat worker ([UptimeRobot](https://uptimerobot.com/)) to maintain 24/7 web service availability

### Tech Stack

- **Backend:** `Python 3.13`, `FastAPI (ASGI core)`, `Flask WSGI via WSGIMiddleware`, `Uvicorn`, `Gunicorn (gevent workers)`, `SQLAlchemy (sync/async)`, `Alembic`, `Marshmallow`, `Pydantic v2`, `Flask-Limiter`, `SlowAPI`, `Tenacity`
- **Database:** `PostgreSQL`
- **Infrastructure & DevOps:** `Docker`, `Docker Compose`, `GitHub Actions (CI/CD)`, `APScheduler (for db clear)`
- **Testing & Quality:** `Pytest`, `unittest.mock`, `Codecov`, `k6 (smoke, load, stress, spike)`
- **API & Docs:** `FastAPI Auto Docs (Swagger/ReDoc)`, `apispec`, `flask-swagger-ui (OpenAPI 3.0)`
- **Observability:** `prometheus-flask-exporter`, `structured JSON logging`
- **Security:** `flask-talisman`
- **Caching:** `Redis`, `redis-py`

## Quick Start (Docker)

> Prefer not to run it locally? Try the [live demo](https://weather-7icc.onrender.com) above.

1. Clone the repo and copy the environment template:

```bash
git clone https://github.com/Weatherender-foundation/Weatherender.git
cd Weatherender
cp .env.example .env
```

2. Fill in `.env` — at minimum you'll need a free API key from [weatherapi.com](https://www.weatherapi.com/) (`WEATHER_API_KEY`) and a `SECRET_KEY`:

```bash
python -c "import secrets; print(secrets.token_hex(32))"
```

3. Start the stack:

> Make sure Docker Desktop is installed

```bash
docker compose up -d
docker compose run --rm cli alembic upgrade head
```

4. Open [http://localhost:5001](http://localhost:5001) for the Flask-only process (UI + sync API).

   Open [http://localhost:8001](http://localhost:8001) for the combined process: FastAPI v2 **and** the same Flask app mounted via `WSGIMiddleware` (this is what production runs). Docs: [http://localhost:8001/v2/docs](http://localhost:8001/v2/docs).

Local ports (from `.env.example`):

| Service | Host port | Serves |
| --- | --- | --- |
| `web` | `5001` | Gunicorn + Flask only (UI, `/api/weather`, `/health`, `/apidocs`) |
| `api` | `8001` | Uvicorn + FastAPI + Flask mounted (v2 **and** v1/UI) |
| PostgreSQL | `5432` | — |
| Redis | `6379` | — |
| Test PostgreSQL | `5433` | — |


## Running the CLI

Via Docker Compose:

```bash
docker compose run --rm cli weatherender
# or:
docker compose run --rm cli python -m weatherender.CLI.main
```

Via the PyPI package (after `pip install weatherender` and a local `.env`):

```bash
weatherender
```

## API

The app exposes a JSON REST API alongside the web UI.

| Endpoint | Method | Description |
| --- | --- | --- |
| `/api/v2/weather` | GET | High-performance async API v2 with Pydantic validation (rate limited: 25 req/min) |
| `/api/v2/health` | GET | Async DB health check |
| `/v2/redoc` | GET | Alternative FastAPI ReDoc documentation |
| `/v2/docs` | GET | Interactive FastAPI OpenAPI/Swagger documentation |
| `/api/weather` | GET | Get current weather + forecast for a city (`?city=Berlin`) |
| `/api/apispec.json` | GET | Raw OpenAPI 3.0 specification (v1) |
| `/health` | GET | Readiness check (verifies DB connectivity) |
| `/metrics` | GET | Prometheus metrics |
| `/api/ping` | GET | Ping endpoint for uptime monitors (no DB connection) |
| `/apidocs` | GET | Interactive Swagger/OpenAPI documentation (v1) |

> `/api/weather` and `/api/v2/weather` responses are cached in Redis for 5 minutes (configurable via `REDIS_TTL`). If Redis is unavailable, the app falls back to fetching fresh data from WeatherAPI directly.

Full reference: [`docs/API.md`](docs/API.md).

## Performance & Caching Strategy

To ensure high performance and minimize reliance on external services, the application implements a multi-layered caching and optimization architecture:

- **Redis Integration:** Weather data fetched from WeatherAPI is cached in an Upstash Redis instance with a 5-minute TTL (`REDIS_TTL`). Subsequent requests for the same city are served instantly from the cache, saving external API quotas.
- **Graceful Degradation:** If the Redis instance becomes temporarily unavailable, the application automatically catches the exception and gracefully falls back to direct API fetching without disrupting the user experience.
- **Database Efficiency:** The automated uptime monitor triggers a lightweight `/api/ping` route that does not open SQLAlchemy sessions or hit the database. This prevents creating redundant connections on the free Supabase tier, keeping the connection pool clean.

## Performance Testing

The application has been load, stress, and spike tested with [k6](https://k6.io/) — both against the live Render deployment and locally via Docker Compose. Full methodology and results, including a real bottleneck investigation (sync → gevent Gunicorn workers), are documented in [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md). Scripts live in `load_tests/`.

### Running Load Tests Locally

k6 must be installed locally (it isn't bundled as a Docker Compose service). Smoke and load tests target the live Render deployment directly; stress and spike tests require the local stack running first.

#### 1. Smoke Test (Render — verify the live deployment is alive and stable)

```bash
k6 run load_tests/smoke.js
```

#### 2. Load Test (Render — realistic traffic across all endpoints)

```bash
k6 run load_tests/load.js
```

#### 3. Stress Test (local — find the system's breaking point) — [running locally](#quick-start-docker)

```bash
docker compose up -d
k6 run load_tests/stress.js
```

#### 4. Spike Test (local — validate resilience against sudden traffic bursts) — [running locally](#quick-start-docker)

```bash
docker compose up -d
k6 run load_tests/spike.js
```

## Further Documentation

- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — component responsibilities, request/data flow, infrastructure
- [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md) — how the production deployment (Render + Supabase + Upstash) is set up and reproduced
- [`docs/API.md`](docs/API.md) — full API reference (endpoints, params, error formats, rate limiting)
- [`docs/PERFORMANCE.md`](docs/PERFORMANCE.md) — load-testing methodology and results
- [`docs/CHANGELOG.md`](docs/CHANGELOG.md) — project history by date/theme

## Running Tests

Tests require a dedicated PostgreSQL test container (kept separate from the dev/prod database):

```bash
docker compose up -d weather_test_db
DATABASE_URL="postgresql://test_user:test_password@localhost:5433/test_weather_db" alembic upgrade head
pytest -v
```

Via Compose (same path as CI):

```bash
docker compose run --rm -v "$PWD":/app cli pytest tests/ --cov=. --cov-report=xml --asyncio-mode=auto
```

Note: `test_cache.py` mocks the Redis client directly and does not require a running Redis instance.

## Engineering Standards & Git Flow

This repository strictly adheres to professional enterprise software development practices:

- **Feature Branching:** Every bug fix, optimization, and component expansion is developed in isolated branches (`feature/*`, `fix/*`) to ensure the `main` branch remains stable and deployable at all times.
- **True Merging:** The project utilizes explicit merge commits via the `--no-ff` (no fast-forward) strategy to preserve a rich, readable, and non-linear history of architectural iterations.
- **Conventional Commits:** Commit messages are standardized using semantic prefixes (`feat:`, `fix:`, `docs:`, `refactor:`, `tests:`) to provide transparency in project growth and documentation maintenance.
- **Issue-Driven Post-Mortems:** Real infrastructure incidents and complex bug fixes are documented in the repository's closed Issues as production post-mortems, tracking root cause analysis and technical resolutions.
- **Automated CI/CD & Safe Deployment:** Completely automated integration via GitHub Actions. The workflow forces strict validation layers (Ruff linting, Mypy type-checking, and full Pytest execution) before triggering a production release. Automated deployment via Render webhooks is strictly gated and will automatically block if any unit test or linting check fails.

## Roadmap & Future Enhancements

The next major architectural evolution of **Weatherender** is fully planned:

- [ ] **Dynamic Radar Maps:** Integrate interactive precipitation radar and dynamic weather maps using GIS/Leaflet tools to visualize snow and rain fronts
- [x] **Asynchronous API v2 (FastAPI Integration):** High-performance `api/v2` microservice using **FastAPI** (`asyncio`, `asyncpg`, `httpx`, `Pydantic v2`) mounted alongside Flask via `WSGIMiddleware`
- [x] **Python package & GHCR image:** Installable `weatherender` on PyPI and published API image on GitHub Container Registry
- [ ] **User Authentication & Custom Alerts:** Implement secure JWT or session-based user authentication via Supabase Auth, allowing skiers to save favorite resorts and customize automated notification limits

## Project Structure & Architecture

The installable package lives under `src/weatherender/`:

```text
src/weatherender/
├── WEB/          # Flask UI + v1 JSON API (Compose `web`, port 5001 — Flask only)
├── API/          # FastAPI v2 + mounted Flask (Compose `api` / Render / GHCR, port 8001)
├── CLI/          # console script: weatherender
├── config.py
├── models.py
├── services.py
├── cache.py
├── schemas.py
└── snow.py
```

Locally, Compose runs `web` (port **5001**, Flask only) and `api` (port **8001**, FastAPI + Flask via `WSGIMiddleware`) as separate containers. Production and GHCR use only the API image, so one uvicorn process serves both stacks — the same shape as local `:8001`.


Full breakdown, component responsibilities, and request/data flow diagrams: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).

![Project architecture](./docs/architecture.svg)

## Engineering Challenges & Bug Investigations

A full technical deep-dive into blockers, edge cases, and architectural updates can be tracked in closed [Project Issues](https://github.com/Weatherender-foundation/Weatherender/issues). Key production-level engineering challenges solved during development include:

### 1. Database Pollution Mitigation & HTTP HEAD Short-Circuiting ([Issue #9](https://github.com/Weatherender-foundation/Weatherender/issues/9))

- **The Incident (Post-Mortem):** Continuous 10-minute uptime checks from [UptimeRobot](https://uptimerobot.com) targeting the root route (`/`) were heavily polluting the operational logs and telemetry within the Supabase PostgreSQL storage layer, triggering unintended database writes and accumulating thousands of empty automated tracking rows.
- **Root Cause Analysis:** The external monitor was explicitly configured to use the `HTTP HEAD` method. However, because Flask by default implicitly converts unhandled `HEAD` requests to `GET` on routes without explicit declaration, the request bypassed custom `User-Agent` string filtering layers. This initiated a redundant `SessionLocal()` DB connection and triggered execution of an unpredictable insert query lifecycle on every health probe.
- **Engineering Solution & Shield:**
  1. Updated the internal `/api/ping` route configuration within `api_routes.py` to explicitly support both `GET` and `HEAD` methods to handle direct infrastructure probes natively.
  2. Overhauled the core index (`/`) route decorator in `app.py` by introducing explicit `HEAD` support (`methods=["GET", "POST", "HEAD"]`).
  3. Placed a high-priority, zero-cost early return check (`if request.method == "HEAD": return ""`) at the very first line of execution.
- **Results:** Database pollution was successfully brought down to **0%**, server resources were completely preserved, and database sessions are now never initialized for non-human monitoring traffic.

### 2. Flask-Limiter Blueprint Registry Mismatch & Circular Dependency Resolution ([Issue #10](https://github.com/Weatherender-foundation/Weatherender/issues/10))

- **The Incident:** During pre-merge manual load testing using a custom `check_limit.sh` script (firing 400 sequential requests), the automated uptime monitor health checks against `/api/ping` consistently failed with `429 Too Many Requests`. The endpoint kept throttling traffic even though an explicit `limiter.exempt(ping)` rule was active in `app.py`.
- **Root Cause Analysis:** A deep-dive revealed a lifecycle mismatch in how `flask-limiter==4.1.1` registers routes. When applying a blueprint-wide limit (`api_bp`), the `@limiter.exempt` decorator failed because it looked up the bare function name, whereas Flask resolves the request's endpoint at dispatch time using the prefixed name (`api.ping`). Furthermore, attempting to apply decorators directly inside `api_routes.py` introduced immediate circular imports between `app.py` and the routing module.
- **Engineering Solution:**
  1. Broke the circular dependency chain by decoupling the `Limiter` instance instantiation, moving it into an uninitialized state inside a newly designed infrastructure layer: `src/weatherender/WEB/extensions.py`.
  2. Late-bound the engine during the application factory setup in `app.py` via `limiter.init_app(app)`.
  3. Dropped the fragile blueprint-wide mapping logic entirely. Instead, explicitly declared per-route limits using `@limiter.limit("25 per minute")` directly on the protected production endpoints (`get_weather` and `get_apispec`), leaving the critical `/api/ping` route cleanly undecorated and inherently immune to rate-limiting blocks.
- **Results:** Re-running the 400-request load test confirmed 100% success on `/api/ping` (returning pure `200 OK` metrics alongside expected gevent network socket resets), while public endpoints correctly isolated and blocked aggressive traffic bursts.

---

## About the Author

```text
It's been developing since I was 13 years old.
```

I'm an active alpine skier (currently holding the 2nd adult sports rank and training for the 1st) and a full-time student at **Gymnasium 1514**. Due to intensive academic tracking and a demanding winter training schedule on the ski slopes, my development velocity temporarily transitions into a maintenance phase during the winter season — full-scale feature development resumes during the next summer cycle.

I also write about the engineering side of this project on my [Habr profile](https://habr.com/en/users/LyapinAlexey/).

---

## License

This project is licensed under the **SSCI Custom License v1.2**.

> 🛡️ **License:** Non-Commercial Use Only
> You may download and use this package for **personal, educational, or non-commercial purposes**.
> **Commercial use (selling, SaaS, monetization) is strictly prohibited** without written permission.
Full license text:
[SSCI Custom License v1.2](./LICENSE)

For commercial licensing inquiries, contact:

<p align="center">
  <a href="mailto:lehacomp16@gmail.com" style="display: inline-block; margin-bottom: 8px;">
    <img src="https://img.shields.io/badge/Email-lehacomp16%40gmail.com-blue?style=for-the-badge&logo=gmail&logoColor=white" height="32" alt="Email" />
  </a>
  <br>
  <a href="https://t.me/LyapinAlexey" style="display: inline-block;">
    <img src="https://img.shields.io/badge/Telegram-%40LyapinAlexey-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white" height="32" alt="Telegram" />
  </a>
</p>
