diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..ac4b4f8
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,15 @@
+# Python version
+PYTHON_VER_MIN_MAJOR=3
+PYTHON_VER_MIN_MINOR=9
+
+# Input data
+INPUT_DAY_HOURS_START=9
+INPUT_DAY_HOURS_END=19
+INPUT_HOUR_CONDITIONS_SCORE="{'clear': 1, 'partly-cloudy': 1, 'cloudy': 1, 'overcast': 1}"
+INPUT_LINKS_PATH="src/core/cities.json"
+INPUT_COND_WEIGHT=0.5
+INPUT_TEMP_WEIGHT=0.5
+INPUT_TOP_LOCATIONS_COUNT=5
+
+# Output data
+OUTPUT_PATH="output.json"
diff --git a/.github/workflows/app-testing.yml b/.github/workflows/app-testing.yml
index 7f2c15c..fbde5a3 100644
--- a/.github/workflows/app-testing.yml
+++ b/.github/workflows/app-testing.yml
@@ -1,31 +1,100 @@
-name: build-and-test
+name: CI
on:
push:
- branches: [ main ]
+ branches: [main]
pull_request:
- branches: [ main ]
+ branches: [main]
jobs:
- build:
+ check:
+
+ runs-on: ubuntu-latest
+
+ outputs:
+ has_changes: ${{ steps.check.outputs.has_changes }}
+ changed_files: ${{ steps.check.outputs.changed_files }}
+
+ steps:
+ # 1. Checkout the code from the repository
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+
+ # 2. Get a list of changed .py files
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v44
+ with:
+ files: |
+ **.py
+
+ # 3. Set outputs for changed files
+ - name: Set change flag and files
+ id: check
+ run: |
+ if [ "${{ steps.changed-files.outputs.any_changed }}" == "true" ]; then
+ echo "has_changes=${{ steps.changed-files.outputs.any_changed }}" >> $GITHUB_OUTPUT
+ echo "changed_files=${{ steps.changed-files.outputs.all_changed_files }}" >> $GITHUB_OUTPUT
+ else
+ echo "has_changes=false" >> $GITHUB_OUTPUT
+ echo "changed_files=" >> $GITHUB_OUTPUT
+ fi
+
+
+ tests:
+ needs: [check]
+ if: ${{ needs.check.outputs.has_changes == 'true' }}
+
runs-on: ubuntu-latest
+
strategy:
matrix:
- python-version: ["3.9", "3.10", "3.11"]
+ python-version: ['3.12', '3.13']
+
steps:
- - uses: actions/checkout@v2
+ # 1. Checkout the code from the repository
+ - name: Checkout Code
+ uses: actions/checkout@v4
+
+ # 2. Rename .env.example file to .env
+ - name: Rename env file
+ run: mv .env.example .env
+
+ # 3. Set up the Python environment
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- - name: Install dependencies
+
+ # 4. Install uv
+ - name: Install uv
+ uses: astral-sh/setup-uv@v3
+
+ # 5. Install python
+ - name: Set up Python
+ run: uv python install
+
+ # 6. Install the project
+ - name: Install the project
+ run: uv sync --all-extras --dev
+
+ # 7. Install missing stub packages
+ - name: Install missing type stubs
+ run: |
+ uv run mypy --install-types --non-interactive .
+
+ # 8. Run Ruff Linter and show all errors
+ - name: Run Ruff Linter
run: |
- python -m pip install --upgrade pip
- pip install flake8 pytest
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- - name: Lint with flake8
+ echo "${{ needs.check.outputs.changed_files }}" | xargs uv run ruff check --output-format=github --config=pyproject.toml
+
+ # 9. Run Mypy Type Checking and show all errors
+ - name: Run Mypy Type Checking
+ run: |
+ echo "${{ needs.check.outputs.changed_files }}" | xargs uv run mypy --config-file=pyproject.toml
+
+ # 10. Run Pytest
+ - name: Run Pytest
run: |
- # stop the build if there are Python syntax errors or undefined names
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- # exit-zero treats all errors as warnings
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=119 --statistics --config=setup.cfg
+ uv run pytest --maxfail=10 --disable-warnings --tb=short
diff --git a/.gitignore b/.gitignore
index 0d73fbe..e7fca24 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,112 @@ creds.ini
.idea/
__pycache__/
*.pyc
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+.idea/**/aws.xml
+.idea/**/contentModel.xml
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+.idea/**/gradle.xml
+.idea/**/libraries
+cmake-build-*/
+.idea/**/mongoSettings.xml
+*.iws
+out/
+.idea_modules/
+atlassian-ide-plugin.xml
+.idea/replstate.xml
+.idea/sonarlint/
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+.idea/httpRequests
+.idea/caches/build_file_checksums.ser
+*.py[cod]
+*$py.class
+*.so
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+*.manifest
+*.spec
+pip-log.txt
+pip-delete-this-directory.txt
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+*.mo
+*.pot
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+instance/
+.webassets-cache
+.scrapy
+docs/_build/
+.pybuilder/
+target/
+.ipynb_checkpoints
+profile_default/
+ipython_config.py
+.pdm.toml
+__pypackages__/
+celerybeat-schedule
+celerybeat.pid
+*.sage.py
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+.spyderproject
+.spyproject
+.ropeproject
+/site
+.mypy_cache/
+.dmypy.json
+dmypy.json
+.pyre/
+.pytype/
+cython_debug/
+mypy_cache/
+ruff_cache/
+
+output.json
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..fbe42e2
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,39 @@
+repos:
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.6.0
+ hooks:
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
+ - id: no-commit-to-branch
+ - id: debug-statements
+ - id: check-json
+ - id: name-tests-test
+ args: [--pytest-test-first]
+ - id: pretty-format-json
+ args: [--autofix, --indent, '2']
+ - id: check-added-large-files
+
+- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
+ rev: v2.14.0
+ hooks:
+ - id: pretty-format-toml
+ args: [--autofix, --indent, '2', --inline-comment-spaces, '2', --trailing-commas]
+ - id: pretty-format-yaml
+ args: [--autofix, --indent, '2']
+
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.6.9
+ hooks:
+ - id: ruff-format
+ args: [--config=pyproject.toml]
+ - id: ruff
+ args: [--config=pyproject.toml, --fix, --exit-non-zero-on-fix]
+
+- repo: https://github.com/pre-commit/mirrors-mypy
+ rev: v1.11.2
+ hooks:
+ - id: mypy
+ args: [--config-file=pyproject.toml, --install-types, --non-interactive]
+ additional_dependencies:
+ - pydantic
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c67ddd4
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,37 @@
+ONESHELL:
+
+.PHONY: env
+env:
+ @find . -name ".env.example" | while read file; do \
+ cp "$$file" "$$(dirname $$file)/.env"; \
+ done
+
+
+.PHONY: sync
+sync:
+ @uv sync --frozen --all-extras
+
+.PHONY: setup
+setup:
+ @curl -LsSf https://astral.sh/uv/install.sh | sh
+
+.PHONY: upd_hooks
+upd_hooks:
+ @pre-commit clean
+ @pre-commit install --install-hooks
+
+.PHONY: check
+check:
+ @git add .
+ @pre-commit run
+
+.PHONY: up
+up: env setup sync
+
+.PHONY: run
+run: sync env
+ @python -m src.main
+
+.PHONY: test
+test:
+ pytest
diff --git a/README.md b/README.md
index 5c3e6eb..5ca4224 100644
--- a/README.md
+++ b/README.md
@@ -1,112 +1,34 @@
-# Проектное задание первого спринта
-
-Ваша задача — проанализировать данные по погодным условиям, полученные от API Яндекс Погоды.
-
-## Описание задания
-
-**1. Получите информацию о погодных условиях для указанного списка городов, используя API Яндекс Погоды.**
-
-
- Описание
-
-Список городов находится в переменной `CITIES` в файле [utils.py](utils.py). Для взаимодействия с API используйте готовый класс `YandexWeatherAPI` в модуле `external/client.py`. Пример работы с классом `YandexWeatherAPI` описан в примере. Пример ответа от API для анализа вы найдёте в [файле](examples/response.json).
-
-
-
-**2. Вычислите среднюю температуру и проанализируйте информацию об осадках за указанный период для всех городов.**
-
-
- Описание
-
-Условия и требования:
-- период вычислений в течение дня — с 9 до 19 часов;
-- средняя температура рассчитывается за указанный промежуток времени;
-- сумма времени (часов), когда погода без осадков (без дождя, снега, града или грозы), рассчитывается за указанный промежуток времени;
-- информация о температуре для указанного дня за определённый час находится по следующему пути: `forecasts> [день]> hours> temp`;
-- информация об осадках для указанного дня за определённый час находится по следующему пути: `forecasts> [день]> hours> condition`.
-
-[Пример данных](examples/response-day-info.png) с информацией о температуре и осадках за день.
-
-Список вариантов погодных условий находится [в таблице в блоке `condition`](https://yandex.ru/dev/weather/doc/dg/concepts/forecast-test.html#resp-format__forecasts) или в [файле](examples/conditions.txt).
-
-Для анализа данных используйте подготовленный скрипт в модуле `external/analyzer.py`. Скрипт имеет два параметра запуска:
-- `-i` – путь до файла с данными, как результат ответа от `YandexWeatherAPI` в формате `json`;
-- `-o` – путь до файла для сохранения результата выполнения работы.
-
-Пример запуска скрипта:
+# Weather Data Analysis
+
+[](https://github.com/astral-sh/uv)
+[](https://github.com/astral-sh/ruff)
+[](https://github.com/agredyaev/async-python-sprint-1/actions)
+
+
+
+[](LICENSE)
+
+## Project Overview
+
+This project analyzes weather conditions using data from the Yandex Weather API. The task involves retrieving weather data for a list of cities, calculating the average temperature and analyzing precipitation conditions for a specific period within a day.
+
+### Key Features
+- **FetchTask**: Fetches weather data from an external API (e.g., YandexWeatherAPI).
+- **ExtractTask**: Extracts and validates relevant weather data.
+- **TransformTask**: Processes and transforms raw weather data into structured formats.
+- **AnalyzeTask**: Analyzes transformed data to compute key metrics like weather scores.
+
+## Deploy
```bash
-python3 external/analyzer.py -i examples/response.json -o output.json
-```
-
-[Пример данных](examples/output.json) с информацией об анализе данных для одного города за период времени, указанный во входном файле.
-
-
-
-
-**3. Объедините полученные данные и сохраните результат в текстовом файле.**
-
-
- Описание
-
-Формат сохраняемого файла – **json**, **yml**, **csv** или **xls/xlsx**.
-
-Возможный формат таблицы для сохранения, где рейтинг — это позиция города относительно других при анализе «благоприятности поездки» (п.4).
-
-| Город/день | | 14-06 | ... | 19-06 | Среднее | Рейтинг |
-|-------------|:--------------------------|:-----:|:---:|:-----:|--------:|--------:|
-| Москва | Температура, среднее | 24 | | 27 | 25.6 | 8 |
-| | Без осадков, часов | 8 | | 4 | 6 | |
-| Абу-Даби | Температура, среднее | 34 | | 37 | 35.5 | 2 |
-| | Без осадков, часов | 9 | | 10 | 9.5 | |
-| ... | | | | | | |
-
-
-
-
-**4. Проанализируйте результат и сделайте вывод, какой из городов наиболее благоприятен для поездки.**
-
-
- Описание
-
-Наиболее благоприятным городом считать тот, в котором средняя температура за всё время была самой высокой, а количество времени без осадков — максимальным.
-Если таких городов более одного, то выводить все.
-
-
-
-## Требования к решению
-
-1. Используйте для решения как процессы, так и потоки. Для этого разделите все задачи по их типу – IO-bound или CPU-bound.
-2. Используйте для решения и очередь, и пул задач.
-3. Опишите этапы решения в виде отдельных классов в модуле [tasks.py](tasks.py):
- - `DataFetchingTask` — получение данных через API;
- - `DataCalculationTask` — вычисление погодных параметров;
- - `DataAggregationTask` — объединение вычисленных данных;
- - `DataAnalyzingTask` — финальный анализ и получение результата.
-4. Используйте концепции ООП.
-5. Предусмотрите обработку исключительных ситуаций.
-6. Логируйте результаты действий.
-7. Используйте аннотацию типов.
-8. Приведите стиль кода в соответствие pep8, flake8, mypy.
-
-
-## Рекомендации к решению
-
-1. Предусмотрите и обработайте ситуации с некорректным обращением к внешнему API: отсутствующая/битая ссылка, неверный ответ, невалидное содержимое или иной формат ответа.
-2. Покройте написанный код тестами.
-3. Используйте таймауты для ограничения времени выполнения частей программы и принудительного завершения при зависаниях или нештатных ситуациях.
-
-
----
-
-
-
-## Пример использования `YandexWeatherAPI` для работы с API
-
-```python
-from external.client import YandexWeatherAPI
-from utils import get_url_by_city_name
-
-city_name = "MOSCOW"
-url_with_data = get_url_by_city_name(city_name)
-resp = YandexWeatherAPI.get_forecasting(data_url)
+# clone the repository
+git clone https://github.com/agredyaev/async-python-sprint-1.git
+cd async-python-sprint-1
+# setup the environment
+make setup
+# activate the virtual environment
+. ./.venv/bin/activate
+# run the app
+make run
+# run the tests
+make test
```
diff --git a/api_test.py b/api_test.py
deleted file mode 100644
index 9dc3952..0000000
--- a/api_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import subprocess
-
-
-def check_python_version():
- from utils import check_python_version
-
- check_python_version()
-
-
-def check_api():
- from external.client import YandexWeatherAPI
- from utils import get_url_by_city_name
-
- CITY_NAME_FOR_TEST = "MOSCOW"
-
- data_url = get_url_by_city_name(CITY_NAME_FOR_TEST)
- resp = YandexWeatherAPI.get_forecasting(data_url)
- all_keys = resp.keys()
- print(all_keys)
- print(resp["info"])
-
- # command_to_execute = [
- # "python3",
- # "./external/analyzer.py",
- # "-i",
- # "./examples/response.json",
- # "-o",
- # "./output.json",
- # ]
- # run = subprocess.run(command_to_execute, capture_output=True)
-
-
-if __name__ == "__main__":
- check_python_version()
- check_api()
diff --git a/examples/conditions.txt b/examples/conditions.txt
deleted file mode 100644
index c9cbbf3..0000000
--- a/examples/conditions.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-Код расшифровки погодного описания. Возможные значения:
-clear — ясно.
-partly-cloudy — малооблачно.
-cloudy — облачно с прояснениями.
-overcast — пасмурно.
-drizzle — морось.
-light-rain — небольшой дождь.
-rain — дождь.
-moderate-rain — умеренно сильный дождь.
-heavy-rain — сильный дождь.
-continuous-heavy-rain — длительный сильный дождь.
-showers — ливень.
-wet-snow — дождь со снегом.
-light-snow — небольшой снег.
-snow — снег.
-snow-showers — снегопад.
-hail — град.
-thunderstorm — гроза.
-thunderstorm-with-rain — дождь с грозой.
-thunderstorm-with-hail — гроза с градом.
\ No newline at end of file
diff --git a/examples/output.json b/examples/output.json
deleted file mode 100644
index ad30299..0000000
--- a/examples/output.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "days": [
- {
- "date": "2022-05-18",
- "hours_start": 9,
- "hours_end": 19,
- "hours_count": 11,
- "temp_avg": 13.091,
- "relevant_cond_hours": 11
- },
- {
- "date": "2022-05-19",
- "hours_start": 9,
- "hours_end": 19,
- "hours_count": 11,
- "temp_avg": 10.727,
- "relevant_cond_hours": 5
- },
- {
- "date": "2022-05-20",
- "hours_start": 9,
- "hours_end": 19,
- "hours_count": 11,
- "temp_avg": 11.364,
- "relevant_cond_hours": 11
- },
- {
- "date": "2022-05-21",
- "hours_start": null,
- "hours_end": null,
- "hours_count": 0,
- "temp_avg": null,
- "relevant_cond_hours": 0
- },
- {
- "date": "2022-05-22",
- "hours_start": null,
- "hours_end": null,
- "hours_count": 0,
- "temp_avg": null,
- "relevant_cond_hours": 0
- }
- ]
-}
\ No newline at end of file
diff --git a/examples/response-day-info.png b/examples/response-day-info.png
deleted file mode 100644
index 598976f..0000000
Binary files a/examples/response-day-info.png and /dev/null differ
diff --git a/examples/response.json b/examples/response.json
deleted file mode 100644
index e7fcdcd..0000000
--- a/examples/response.json
+++ /dev/null
@@ -1,1933 +0,0 @@
-{
- "now": 1652833102,
- "now_dt": "2022-05-18T00:18:22.763458Z",
- "info": {
- "n": true,
- "geoid": 213,
- "url": "https://yandex.com/weather/213?lat=55.753&lon=37.616",
- "lat": 55.753,
- "lon": 37.616,
- "tzinfo": {
- "name": "Europe/Moscow",
- "abbr": "MSK",
- "dst": false,
- "offset": 10800
- },
- "def_pressure_mm": 745,
- "def_pressure_pa": 993,
- "slug": "213",
- "zoom": 10,
- "nr": true,
- "ns": true,
- "nsr": true,
- "p": false,
- "f": true,
- "_h": false
- },
- "geo_object": {
- "district": {
- "id": 120540,
- "name": "Tverskoy District"
- },
- "locality": {
- "id": 213,
- "name": "Moscow"
- },
- "province": {
- "id": 213,
- "name": "Moscow"
- },
- "country": {
- "id": 225,
- "name": "Russian Federation"
- }
- },
- "yesterday": {
- "temp": 8
- },
- "fact": {
- "obs_time": 1652832000,
- "uptime": 1652833102,
- "temp": 9,
- "feels_like": 5,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_prob": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_speed": 3.6,
- "wind_dir": "n",
- "pressure_mm": 741,
- "pressure_pa": 987,
- "humidity": 79,
- "daytime": "n",
- "polar": false,
- "season": "spring",
- "source": "station",
- "accum_prec": {
- "1": 18.13,
- "7": 29.914318,
- "3": 22.819426
- },
- "soil_moisture": 0.34,
- "soil_temp": 8,
- "uv_index": 0,
- "wind_gust": 7.8
- },
- "forecasts": [
- {
- "date": "2022-05-18",
- "date_ts": 1652821200,
- "week": 20,
- "sunrise": "04:13",
- "sunset": "20:38",
- "rise_begin": "03:22",
- "set_end": "21:29",
- "moon_code": 1,
- "moon_text": "moon-code-1",
- "hours": [
- {
- "hour": "0",
- "hour_ts": 1652821200,
- "temp": 10,
- "feels_like": 7,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.3,
- "wind_gust": 7.5,
- "pressure_mm": 740,
- "pressure_pa": 986,
- "humidity": 81,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "1",
- "hour_ts": 1652824800,
- "temp": 9,
- "feels_like": 6,
- "icon": "ovc_ra",
- "condition": "rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.5,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.6,
- "wind_gust": 7.5,
- "pressure_mm": 740,
- "pressure_pa": 986,
- "humidity": 83,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.34,
- "prec_mm": 0.5,
- "prec_period": 60,
- "prec_prob": 90
- },
- {
- "hour": "2",
- "hour_ts": 1652828400,
- "temp": 9,
- "feels_like": 6,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.6,
- "wind_gust": 7.5,
- "pressure_mm": 740,
- "pressure_pa": 986,
- "humidity": 81,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "3",
- "hour_ts": 1652832000,
- "temp": 9,
- "feels_like": 6,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.5,
- "wind_gust": 7.8,
- "pressure_mm": 741,
- "pressure_pa": 987,
- "humidity": 80,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "4",
- "hour_ts": 1652835600,
- "temp": 9,
- "feels_like": 5,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.9,
- "wind_gust": 7.8,
- "pressure_mm": 741,
- "pressure_pa": 987,
- "humidity": 79,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "5",
- "hour_ts": 1652839200,
- "temp": 8,
- "feels_like": 5,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.8,
- "wind_gust": 7.8,
- "pressure_mm": 741,
- "pressure_pa": 987,
- "humidity": 78,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "6",
- "hour_ts": 1652842800,
- "temp": 8,
- "feels_like": 5,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.6,
- "wind_gust": 7.7,
- "pressure_mm": 742,
- "pressure_pa": 989,
- "humidity": 79,
- "uv_index": 0,
- "soil_temp": 7,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "7",
- "hour_ts": 1652846400,
- "temp": 9,
- "feels_like": 5,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.3,
- "wind_gust": 7.7,
- "pressure_mm": 742,
- "pressure_pa": 989,
- "humidity": 73,
- "uv_index": 1,
- "soil_temp": 7,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "8",
- "hour_ts": 1652850000,
- "temp": 10,
- "feels_like": 6,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.4,
- "wind_gust": 7.7,
- "pressure_mm": 742,
- "pressure_pa": 989,
- "humidity": 69,
- "uv_index": 1,
- "soil_temp": 7,
- "soil_moisture": 0.34,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "9",
- "hour_ts": 1652853600,
- "temp": 11,
- "feels_like": 7,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.4,
- "wind_gust": 9.8,
- "pressure_mm": 743,
- "pressure_pa": 990,
- "humidity": 63,
- "uv_index": 2,
- "soil_temp": 10,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "10",
- "hour_ts": 1652857200,
- "temp": 12,
- "feels_like": 8,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.8,
- "wind_gust": 9.8,
- "pressure_mm": 743,
- "pressure_pa": 990,
- "humidity": 59,
- "uv_index": 3,
- "soil_temp": 10,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "11",
- "hour_ts": 1652860800,
- "temp": 13,
- "feels_like": 9,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.7,
- "wind_gust": 9.8,
- "pressure_mm": 742,
- "pressure_pa": 989,
- "humidity": 56,
- "uv_index": 4,
- "soil_temp": 10,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "12",
- "hour_ts": 1652864400,
- "temp": 14,
- "feels_like": 10,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 5.1,
- "wind_gust": 11.6,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 47,
- "uv_index": 5,
- "soil_temp": 13,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "13",
- "hour_ts": 1652868000,
- "temp": 14,
- "feels_like": 9,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 5.6,
- "wind_gust": 11.6,
- "pressure_mm": 743,
- "pressure_pa": 990,
- "humidity": 45,
- "uv_index": 4,
- "soil_temp": 13,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "14",
- "hour_ts": 1652871600,
- "temp": 15,
- "feels_like": 10,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 5.6,
- "wind_gust": 11.6,
- "pressure_mm": 743,
- "pressure_pa": 990,
- "humidity": 45,
- "uv_index": 4,
- "soil_temp": 13,
- "soil_moisture": 0.33,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "15",
- "hour_ts": 1652875200,
- "temp": 14,
- "feels_like": 9,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.8,
- "wind_gust": 12.3,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 48,
- "uv_index": 3,
- "soil_temp": 14,
- "soil_moisture": 0.32,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "16",
- "hour_ts": 1652878800,
- "temp": 14,
- "feels_like": 8,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 6.1,
- "wind_gust": 12.3,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 49,
- "uv_index": 1,
- "soil_temp": 14,
- "soil_moisture": 0.32,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "17",
- "hour_ts": 1652882400,
- "temp": 13,
- "feels_like": 7,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 6.1,
- "wind_gust": 12.3,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 49,
- "uv_index": 1,
- "soil_temp": 14,
- "soil_moisture": 0.32,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "18",
- "hour_ts": 1652886000,
- "temp": 12,
- "feels_like": 7,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.3,
- "wind_gust": 11.6,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 53,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "19",
- "hour_ts": 1652889600,
- "temp": 12,
- "feels_like": 7,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.3,
- "wind_gust": 11.6,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 55,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "20",
- "hour_ts": 1652893200,
- "temp": 11,
- "feels_like": 6,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 4.8,
- "wind_gust": 11.6,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 56,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "21",
- "hour_ts": 1652896800,
- "temp": 11,
- "feels_like": 7,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.1,
- "wind_gust": 8.3,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 52,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "22",
- "hour_ts": 1652900400,
- "temp": 10,
- "feels_like": 6,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.4,
- "wind_gust": 8.3,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 57,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "23",
- "hour_ts": 1652904000,
- "temp": 9,
- "feels_like": 5,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.1,
- "wind_gust": 8.3,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 60,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- }
- ],
- "biomet": {
- "index": 0,
- "condition": "magnetic-field_0"
- }
- },
- {
- "date": "2022-05-19",
- "date_ts": 1652907600,
- "week": 20,
- "sunrise": "04:11",
- "sunset": "20:40",
- "rise_begin": "03:20",
- "set_end": "21:31",
- "moon_code": 2,
- "moon_text": "moon-code-2",
- "hours": [
- {
- "hour": "0",
- "hour_ts": 1652907600,
- "temp": 9,
- "feels_like": 6,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 1.8,
- "wind_gust": 4.1,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 65,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "1",
- "hour_ts": 1652911200,
- "temp": 9,
- "feels_like": 6,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 2,
- "wind_gust": 4.1,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 68,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "2",
- "hour_ts": 1652914800,
- "temp": 9,
- "feels_like": 6,
- "icon": "ovc",
- "condition": "overcast",
- "cloudness": 1,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 2,
- "wind_gust": 4.1,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 67,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "3",
- "hour_ts": 1652918400,
- "temp": 8,
- "feels_like": 4,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 3.3,
- "wind_gust": 8,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 68,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "4",
- "hour_ts": 1652922000,
- "temp": 8,
- "feels_like": 4,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 3.8,
- "wind_gust": 8,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 69,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "5",
- "hour_ts": 1652925600,
- "temp": 8,
- "feels_like": 4,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 3.7,
- "wind_gust": 8,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 70,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "6",
- "hour_ts": 1652929200,
- "temp": 8,
- "feels_like": 4,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 3.9,
- "wind_gust": 9.2,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 67,
- "uv_index": 0,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "7",
- "hour_ts": 1652932800,
- "temp": 9,
- "feels_like": 4,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 4.6,
- "wind_gust": 9.2,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 65,
- "uv_index": 1,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "8",
- "hour_ts": 1652936400,
- "temp": 10,
- "feels_like": 5,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "w",
- "wind_speed": 4.7,
- "wind_gust": 9.2,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 62,
- "uv_index": 1,
- "soil_temp": 8,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "9",
- "hour_ts": 1652940000,
- "temp": 10,
- "feels_like": 5,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.2,
- "wind_gust": 11.7,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 63,
- "uv_index": 2,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "10",
- "hour_ts": 1652943600,
- "temp": 9,
- "feels_like": 4,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.9,
- "wind_gust": 11.7,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 66,
- "uv_index": 1,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "11",
- "hour_ts": 1652947200,
- "temp": 9,
- "feels_like": 4,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 5.7,
- "wind_gust": 11.7,
- "pressure_mm": 744,
- "pressure_pa": 991,
- "humidity": 67,
- "uv_index": 2,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "12",
- "hour_ts": 1652950800,
- "temp": 10,
- "feels_like": 6,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.6,
- "wind_gust": 8.2,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 71,
- "uv_index": 2,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "13",
- "hour_ts": 1652954400,
- "temp": 10,
- "feels_like": 7,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 4.4,
- "wind_gust": 8.2,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 66,
- "uv_index": 3,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "14",
- "hour_ts": 1652958000,
- "temp": 11,
- "feels_like": 7,
- "icon": "ovc_-ra",
- "condition": "light-rain",
- "cloudness": 1,
- "prec_type": 1,
- "prec_strength": 0.25,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 4.5,
- "wind_gust": 8.2,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 65,
- "uv_index": 3,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0.2,
- "prec_period": 60,
- "prec_prob": 20
- },
- {
- "hour": "15",
- "hour_ts": 1652961600,
- "temp": 11,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.7,
- "wind_gust": 8.3,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 60,
- "uv_index": 3,
- "soil_temp": 11,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "16",
- "hour_ts": 1652965200,
- "temp": 12,
- "feels_like": 7,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 4.2,
- "wind_gust": 8.3,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 55,
- "uv_index": 2,
- "soil_temp": 11,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "17",
- "hour_ts": 1652968800,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.8,
- "wind_gust": 8.3,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 54,
- "uv_index": 1,
- "soil_temp": 11,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "18",
- "hour_ts": 1652972400,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.3,
- "wind_gust": 8.5,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 50,
- "uv_index": 1,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "19",
- "hour_ts": 1652976000,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3.3,
- "wind_gust": 8.5,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 53,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "20",
- "hour_ts": 1652979600,
- "temp": 11,
- "feels_like": 7,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 3,
- "wind_gust": 8.5,
- "pressure_mm": 745,
- "pressure_pa": 993,
- "humidity": 57,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "21",
- "hour_ts": 1652983200,
- "temp": 10,
- "feels_like": 8,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.2,
- "wind_gust": 4,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 60,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "22",
- "hour_ts": 1652986800,
- "temp": 9,
- "feels_like": 6,
- "icon": "bkn_n",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.3,
- "wind_gust": 4,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 65,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "23",
- "hour_ts": 1652990400,
- "temp": 8,
- "feels_like": 6,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.2,
- "wind_gust": 4,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 69,
- "uv_index": 0,
- "soil_temp": 10,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- }
- ],
- "biomet": {
- "index": 0,
- "condition": "magnetic-field_0"
- }
- },
- {
- "date": "2022-05-20",
- "date_ts": 1652994000,
- "week": 20,
- "sunrise": "04:09",
- "sunset": "20:42",
- "rise_begin": "03:18",
- "set_end": "21:33",
- "moon_code": 2,
- "moon_text": "moon-code-2",
- "hours": [
- {
- "hour": "0",
- "hour_ts": 1652994000,
- "temp": 7,
- "feels_like": 5,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.1,
- "wind_gust": 4.2,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 72,
- "uv_index": 0,
- "soil_temp": 7,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "1",
- "hour_ts": 1652997600,
- "temp": 7,
- "feels_like": 4,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.1,
- "wind_gust": 4.2,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 76,
- "uv_index": 0,
- "soil_temp": 7,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "2",
- "hour_ts": 1653001200,
- "temp": 6,
- "feels_like": 4,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "nw",
- "wind_speed": 1.3,
- "wind_gust": 4.2,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 77,
- "uv_index": 0,
- "soil_temp": 7,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "3",
- "hour_ts": 1653004800,
- "temp": 6,
- "feels_like": 3,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 1.8,
- "wind_gust": 6.3,
- "pressure_mm": 746,
- "pressure_pa": 994,
- "humidity": 76,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "4",
- "hour_ts": 1653008400,
- "temp": 5,
- "feels_like": 2,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2,
- "wind_gust": 6.3,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 78,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "5",
- "hour_ts": 1653012000,
- "temp": 6,
- "feels_like": 3,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2,
- "wind_gust": 6.3,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 77,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "6",
- "hour_ts": 1653015600,
- "temp": 7,
- "feels_like": 3,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2.3,
- "wind_gust": 8,
- "pressure_mm": 749,
- "pressure_pa": 998,
- "humidity": 73,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "7",
- "hour_ts": 1653019200,
- "temp": 7,
- "feels_like": 4,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3,
- "wind_gust": 8,
- "pressure_mm": 749,
- "pressure_pa": 998,
- "humidity": 67,
- "uv_index": 1,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "8",
- "hour_ts": 1653022800,
- "temp": 8,
- "feels_like": 4,
- "icon": "skc_d",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.1,
- "wind_gust": 8,
- "pressure_mm": 749,
- "pressure_pa": 998,
- "humidity": 63,
- "uv_index": 1,
- "soil_temp": 6,
- "soil_moisture": 0.31,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "9",
- "hour_ts": 1653026400,
- "temp": 9,
- "feels_like": 4,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.6,
- "wind_gust": 11.2,
- "pressure_mm": 750,
- "pressure_pa": 999,
- "humidity": 58,
- "uv_index": 2,
- "soil_temp": 8,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "10",
- "hour_ts": 1653030000,
- "temp": 9,
- "feels_like": 5,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.1,
- "wind_gust": 11.2,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 55,
- "uv_index": 3,
- "soil_temp": 8,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "11",
- "hour_ts": 1653033600,
- "temp": 10,
- "feels_like": 6,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 4.2,
- "wind_gust": 11.2,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 51,
- "uv_index": 4,
- "soil_temp": 8,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "12",
- "hour_ts": 1653037200,
- "temp": 11,
- "feels_like": 7,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.3,
- "wind_gust": 9.9,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 45,
- "uv_index": 5,
- "soil_temp": 11,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "13",
- "hour_ts": 1653040800,
- "temp": 11,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.7,
- "wind_gust": 9.9,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 43,
- "uv_index": 5,
- "soil_temp": 11,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "14",
- "hour_ts": 1653044400,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.7,
- "wind_gust": 9.9,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 41,
- "uv_index": 4,
- "soil_temp": 11,
- "soil_moisture": 0.3,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "15",
- "hour_ts": 1653048000,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.4,
- "wind_gust": 9.8,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 36,
- "uv_index": 3,
- "soil_temp": 13,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "16",
- "hour_ts": 1653051600,
- "temp": 13,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.5,
- "wind_gust": 9.8,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 34,
- "uv_index": 2,
- "soil_temp": 13,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "17",
- "hour_ts": 1653055200,
- "temp": 13,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 3.4,
- "wind_gust": 9.8,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 34,
- "uv_index": 1,
- "soil_temp": 13,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "18",
- "hour_ts": 1653058800,
- "temp": 13,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2.8,
- "wind_gust": 9.3,
- "pressure_mm": 747,
- "pressure_pa": 995,
- "humidity": 36,
- "uv_index": 1,
- "soil_temp": 12,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "19",
- "hour_ts": 1653062400,
- "temp": 12,
- "feels_like": 8,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2.7,
- "wind_gust": 9.3,
- "pressure_mm": 747,
- "pressure_pa": 995,
- "humidity": 41,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "20",
- "hour_ts": 1653066000,
- "temp": 11,
- "feels_like": 7,
- "icon": "bkn_d",
- "condition": "cloudy",
- "cloudness": 0.5,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "n",
- "wind_speed": 2.6,
- "wind_gust": 9.3,
- "pressure_mm": 747,
- "pressure_pa": 995,
- "humidity": 45,
- "uv_index": 0,
- "soil_temp": 12,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "21",
- "hour_ts": 1653069600,
- "temp": 10,
- "feels_like": 7,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "ne",
- "wind_speed": 1.5,
- "wind_gust": 4.2,
- "pressure_mm": 748,
- "pressure_pa": 997,
- "humidity": 49,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "22",
- "hour_ts": 1653073200,
- "temp": 8,
- "feels_like": 5,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "ne",
- "wind_speed": 1.6,
- "wind_gust": 4.2,
- "pressure_mm": 750,
- "pressure_pa": 999,
- "humidity": 54,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "23",
- "hour_ts": 1653076800,
- "temp": 7,
- "feels_like": 4,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "ne",
- "wind_speed": 1.8,
- "wind_gust": 4.2,
- "pressure_mm": 750,
- "pressure_pa": 999,
- "humidity": 58,
- "uv_index": 0,
- "soil_temp": 9,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- }
- ],
- "biomet": {
- "index": 0,
- "condition": "magnetic-field_0"
- }
- },
- {
- "date": "2022-05-21",
- "date_ts": 1653080400,
- "week": 20,
- "sunrise": "04:08",
- "sunset": "20:44",
- "rise_begin": "03:16",
- "set_end": "21:35",
- "moon_code": 3,
- "moon_text": "moon-code-3",
- "hours": [
- {
- "hour": "0",
- "hour_ts": 1653080400,
- "temp": 7,
- "feels_like": 4,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "ne",
- "wind_speed": 1.1,
- "wind_gust": 3.3,
- "pressure_mm": 749,
- "pressure_pa": 998,
- "humidity": 63,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- },
- {
- "hour": "1",
- "hour_ts": 1653084000,
- "temp": 6,
- "feels_like": 3,
- "icon": "skc_n",
- "condition": "clear",
- "cloudness": 0,
- "prec_type": 0,
- "prec_strength": 0,
- "is_thunder": false,
- "wind_dir": "ne",
- "wind_speed": 1.1,
- "wind_gust": 3.3,
- "pressure_mm": 749,
- "pressure_pa": 998,
- "humidity": 67,
- "uv_index": 0,
- "soil_temp": 6,
- "soil_moisture": 0.29,
- "prec_mm": 0,
- "prec_period": 60,
- "prec_prob": 0
- }
- ]
- },
- {
- "date": "2022-05-22",
- "date_ts": 1653166800,
- "week": 20,
- "sunrise": "04:06",
- "sunset": "20:45",
- "rise_begin": "03:14",
- "set_end": "21:37",
- "moon_code": 3,
- "moon_text": "moon-code-3",
- "hours": []
- }
- ]
-}
\ No newline at end of file
diff --git a/external/analyzer.py b/external/analyzer.py
deleted file mode 100644
index 56663e1..0000000
--- a/external/analyzer.py
+++ /dev/null
@@ -1,217 +0,0 @@
-import argparse
-import json
-import logging
-from dataclasses import dataclass, field
-from functools import reduce
-from operator import getitem
-from typing import Optional, List, Dict
-
-PATH_FROM_INPUT = "./../examples/response.json"
-PATH_TO_OUTPUT = "./../examples/output.json"
-
-INPUT_FORECAST_PATH = "forecasts"
-INPUT_DATE_PATH = "date"
-
-INPUT_HOURS_PATH = "hours"
-INPUT_HOUR_PATH = "hour"
-INPUT_TEMPERATURE_PATH = "temp"
-INPUT_CONDITION_PATH = "condition"
-INPUT_DAY_HOURS_START = 9
-INPUT_DAY_HOURS_END = 19
-INPUT_DAY_SUITABLE_CONDITIONS = [
- "clear",
- "partly-cloudy",
- "cloudy",
- "overcast",
- # "drizzle",
- # "light-rain",
- # "rain",
- # "moderate-rain",
- # "heavy-rain",
- # "continuous-heavy-rain",
- # "showers",
- # "wet-snow",
- # "light-snow",
- # "snow",
- # "snow-showers",
- # "hail",
- # "thunderstorm",
- # "thunderstorm-with-rain",
- # "thunderstorm-with-hail"
-]
-
-OUTPUT_RAW_DATA_KEY = "raw_data"
-OUTPUT_DAYS_KEY = "days"
-DEFAULT_OUTPUT_RESULT = {
- OUTPUT_DAYS_KEY: [],
- # OUTPUT_RAW_DATA_KEY: None,
-}
-
-
-def deep_getitem(obj, path: str):
- try:
- return reduce(getitem, path.split(">"), obj)
- except (KeyError, TypeError):
- return None
-
-
-def load_data(input_path: str = PATH_FROM_INPUT):
- with open(input_path) as file:
- data = file.read()
- return json.loads(data)
-
-
-def dump_data(data, output_path: str = PATH_TO_OUTPUT):
- with open(output_path, mode="w") as file:
- formatted_data = json.dumps(data, indent=2)
- file.write(formatted_data)
-
-
-def parse_args():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "-i",
- "--input",
- default=PATH_FROM_INPUT,
- type=str,
- help="path to file with input data",
- )
- parser.add_argument(
- "-o",
- "--output",
- default=PATH_TO_OUTPUT,
- type=str,
- help="path to file with result",
- )
- parser.add_argument("-v", "--verbose", action="store_true")
- return parser.parse_args()
-
-
-@dataclass
-class HourInfo:
- raw_data: Dict[str, tuple[str, int]] = field(repr=False)
- condition: Optional[str] = field(init=False, default=None)
- temperature: Optional[int] = field(init=False, default=None)
- hour: Optional[int] = field(init=False, default=None)
-
- @staticmethod
- def is_hour_suitable(data):
- hour = int(data[INPUT_HOUR_PATH])
- return (hour >= INPUT_DAY_HOURS_START) and (hour <= INPUT_DAY_HOURS_END)
-
- @property
- def is_cond_suitable(self):
- return self.condition in INPUT_DAY_SUITABLE_CONDITIONS
-
- def __post_init__(self):
- self.parse()
-
- def parse(self):
- if not self.raw_data:
- return
-
- self.hour = int(self.raw_data[INPUT_HOUR_PATH])
- self.temperature = int(deep_getitem(self.raw_data, INPUT_TEMPERATURE_PATH))
- self.condition = deep_getitem(self.raw_data, INPUT_CONDITION_PATH)
-
-
-@dataclass
-class DayInfo:
- raw_data: Dict[str, tuple[str, int]] = field(repr=False)
- hours: Optional[List[HourInfo]] = field(init=False, repr=False, default=None)
-
- date: Optional[str] = field(init=False, default=None)
- hour_start: Optional[int] = field(init=False, default=None)
- hour_end: Optional[int] = field(init=False, default=None)
-
- hours_count: Optional[int] = field(init=False, default=None)
- temperature_avg: Optional[float] = field(init=False, default=None)
- relevant_condition_hours: int = field(init=False, default=0)
-
- def to_json(self):
- return {
- "date": self.date,
- "hours_start": self.hour_start,
- "hours_end": self.hour_end,
- "hours_count": self.hours_count,
- "temp_avg": round(self.temperature_avg, 3)
- if self.temperature_avg
- else self.temperature_avg,
- "relevant_cond_hours": self.relevant_condition_hours,
- }
-
- def __post_init__(self):
- self.parse()
-
- def parse(self):
- if not self.raw_data:
- return
-
- self.date = self.raw_data[INPUT_DATE_PATH]
-
- temp = 0
- hours_count = 0
- conds_count = 0
-
- self.hours = self.raw_data[INPUT_HOURS_PATH]
- # ToDo force sort by hour key in asc mode
- for hour_data in self.hours:
- if not HourInfo.is_hour_suitable(hour_data):
- continue
-
- h_info = HourInfo(raw_data=hour_data)
- h_hour = h_info.hour
- self.hour_start = self.hour_start or h_hour
- self.hour_end = h_hour
-
- temp += h_info.temperature
- if h_info.is_cond_suitable:
- conds_count += 1
- hours_count += 1
-
- self.relevant_condition_hours = conds_count
- self.hours_count = hours_count
- if hours_count > 0:
- self.temperature_avg = temp / hours_count
-
-
-def analyze_json(data):
- if not data:
- logging.warning("Input data is empty...")
- return {}
-
- # analyzing days
- time_start = None
- time_end = None
-
- days_data = deep_getitem(data, INPUT_FORECAST_PATH)
- days = []
- # ToDo force sort by day in asc mode
- for day_data in days_data:
- d_info = DayInfo(raw_data=day_data)
- d_date = d_info.date
-
- time_start = time_start or d_date
- time_end = d_date
-
- days.append(d_info.to_json())
-
- result = DEFAULT_OUTPUT_RESULT
- # result[OUTPUT_RAW_DATA_KEY] = data
- result[OUTPUT_DAYS_KEY] = days
- return result
-
-
-if __name__ == "__main__":
- args = parse_args()
- input_path = args.input
- output_path = args.output
- verbose_mode = args.verbose
-
- logging.basicConfig(level=logging.DEBUG if verbose_mode else logging.WARNING)
- logging.info(args)
-
- data = load_data(input_path)
- data = analyze_json(data)
-
- dump_data(data, output_path)
diff --git a/external/client.py b/external/client.py
deleted file mode 100644
index 04259f3..0000000
--- a/external/client.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import json
-import logging
-from http import HTTPStatus
-from urllib.request import urlopen
-
-ERR_MESSAGE_TEMPLATE = "Unexpected error: {error}"
-
-
-logger = logging.getLogger()
-
-
-class YandexWeatherAPI:
- """
- Base class for requests
- """
-
- def __do_req(url: str) -> str:
- """Base request method"""
- try:
- with urlopen(url) as response:
- resp_body = response.read().decode("utf-8")
- data = json.loads(resp_body)
- if response.status != HTTPStatus.OK:
- raise Exception(
- "Error during execute request. {}: {}".format(
- resp_body.status, resp_body.reason
- )
- )
- return data
- except Exception as ex:
- logger.error(ex)
- raise Exception(ERR_MESSAGE_TEMPLATE.format(error=ex))
-
- @staticmethod
- def get_forecasting(url: str):
- """
- :param url: url_to_json_data as str
- :return: response data as json
- """
- return YandexWeatherAPI.__do_req(url)
diff --git a/forecasting.py b/forecasting.py
deleted file mode 100644
index 2a9ae1c..0000000
--- a/forecasting.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# import logging
-# import threading
-# import subprocess
-# import multiprocessing
-
-
-from external.client import YandexWeatherAPI
-from tasks import (
- DataFetchingTask,
- DataCalculationTask,
- DataAggregationTask,
- DataAnalyzingTask,
-)
-from utils import CITIES, get_url_by_city_name
-
-
-def forecast_weather():
- """
- Анализ погодных условий по городам
- """
- # city_name = "MOSCOW"
- # url_with_data = get_url_by_city_name(city_name)
- # resp = YandexWeatherAPI.get_forecasting(url_with_data)
- # print(resp)
- pass
-
-
-if __name__ == "__main__":
- forecast_weather()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..a7a0836
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,151 @@
+[coverage.report]
+show_missing = true
+
+[coverage.run]
+branch = true
+
+[project]
+dependencies = [
+ "pydantic>=2.9.2",
+ "pydantic-settings>=2.5.2",
+ "httpx>=0.27.2",
+ "polars>=1.9.0",
+]
+description = "Add your description here"
+name = "async-python-sprint-1"
+readme = "README.md"
+requires-python = ">=3.12"
+version = "0.5.0"
+
+[tool.coverage.run]
+omit = ["*/tests/*"]
+
+[tool.mypy]
+allow_redefinition = false
+check_untyped_defs = true
+disallow_subclassing_any = false
+disallow_untyped_decorators = false
+explicit_package_bases = true
+follow_imports = "skip"
+ignore_missing_imports = true
+implicit_reexport = false
+plugins = ["pydantic.mypy"] # https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
+pretty = true
+strict = true
+warn_return_any = false
+
+[[tool.mypy.overrides]]
+ignore_errors = true
+module = ["tests.*"]
+
+[tool.pydantic-mypy]
+init_forbid_extra = true
+init_typed = true
+warn_required_dynamic_aliases = true
+
+[tool.pytest.ini_options]
+addopts = "--strict-config --strict-markers"
+filterwarnings = [
+ "ignore:.*pkg_resources.declare_namespace\\('sphinxcontrib'\\).*:DeprecationWarning",
+ "ignore:pkg_resources is deprecated as an API:DeprecationWarning",
+]
+markers = [
+ # Marks tests that use `attrs` library
+ "attrs",
+]
+
+[tool.ruff]
+force-exclude = true
+line-length = 120
+respect-gitignore = true
+
+[tool.ruff.format]
+docstring-code-format = true
+indent-style = "space"
+quote-style = "double"
+skip-magic-trailing-comma = true
+
+[tool.ruff.lint]
+ignore = [
+ "D", # pydocstyle
+ "TD", # flake8-todos
+ "FIX", # flake8-fixme
+ "ANN", # flake8-annotations (handled by mypy)
+ "EM", # flake8-errmsg - String literals in exceptions
+ "E501", # pycodestyle - line too long (handled by formatter)
+ "COM812", # forced by ruff formatter
+ "ISC001", # forced by ruff formatter
+ "TRY003", # long message for exceptions
+ "EM101", # allow string literals for exceptions
+ "EM102", # allow f-string literals for exceptions
+ "TCH001",
+ "TCH003",
+ "TCH004",
+]
+select = ["ALL"]
+unfixable = [
+ "F", # pyflakes
+ "ERA", # eradicate - commented-out code
+]
+
+[tool.ruff.lint.flake8-tidy-imports] # https://docs.astral.sh/ruff/settings/#lintflake8-tidy-imports
+ban-relative-imports = "all"
+
+[tool.ruff.lint.flake8-type-checking]
+exempt-modules = ["typing", "typing_extensions"]
+quote-annotations = true # https://docs.astral.sh/ruff/settings/#lint_flake8-type-checking_quote-annotations
+
+[tool.ruff.lint.isort] # https://docs.astral.sh/ruff/settings/#isort
+known-first-party = ["src", "tests"]
+lines-between-types = 1
+section-order = [
+ "future",
+ "typing",
+ "standard-library",
+ "third-party",
+ "first-party",
+ "local-folder",
+]
+split-on-trailing-comma = false
+
+[tool.ruff.lint.isort.sections]
+"typing" = ["typing", "types", "typing_extensions", "mypy", "mypy_extensions"]
+
+[tool.ruff.lint.mccabe]
+max-complexity = 10
+
+[tool.ruff.lint.per-file-ignores]
+"**/tests/**/*.py" = [
+ "S101", # Use of assert detected (assert allowed in tests)
+ "PLR2004", # Magic value used in comparison
+]
+"__init__.py" = [
+ "F401", # Unused import
+ "F403", # Import star
+ "ARG001", # Unused function argument (using fixtures from other fixtures)
+]
+
+[tool.ruff.lint.pycodestyle] # https://docs.astral.sh/ruff/settings/#pycodestyle
+max-doc-length = 100
+
+[tool.ruff.lint.pydocstyle] # https://docs.astral.sh/ruff/settings/#pydocstyle
+convention = "google"
+
+[tool.ruff.lint.pylint] # https://docs.astral.sh/ruff/settings/#pylint
+allow-dunder-method-names = ["__tablename__", "__table_args__"]
+allow-magic-value-types = ["int", "str", "bytes"]
+max-args = 10
+max-statements = 30
+
+[tool.uv]
+dev-dependencies = [
+ "coverage>=7.6.1",
+ "mypy>=1.11.2",
+ "pre-commit>=3.8.0",
+ "pytest>=8.3.3",
+ "pytest-cov>=5.0.0",
+ "ruff>=0.6.9",
+ "pyright>=1.1.384",
+ "polyfactory>=2.17.0",
+ "pytest-mock>=3.14.0",
+]
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 9e61d1f..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,12 +0,0 @@
-[flake8]
-ignore =
- W503,
- F811
-exclude =
- tests/,
- */migrations/,
- venv/,
- env/
-per-file-ignores =
- */settings.py:E501
-max-complexity = 10
diff --git a/external/__init__.py b/src/__init__.py
similarity index 100%
rename from external/__init__.py
rename to src/__init__.py
diff --git a/src/clients/__init__.py b/src/clients/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/clients/yandex_weather.py b/src/clients/yandex_weather.py
new file mode 100644
index 0000000..019f058
--- /dev/null
+++ b/src/clients/yandex_weather.py
@@ -0,0 +1,39 @@
+from typing import Any
+
+from http import HTTPStatus
+
+import httpx
+
+from pydantic_core import from_json
+
+from src.core.exceptions import HTTPRequestError
+from src.core.logger import get_logger
+
+logger = get_logger("YandexWeatherAPI")
+
+
+class YandexWeatherAPI:
+ """Base class for handling weather-related requests."""
+
+ __slots__ = ("url",)
+
+ def __init__(self, url: str) -> None:
+ self.url = url
+
+ def _do_req(self) -> httpx.Response:
+ """Asynchronous request method using httpx."""
+ response = httpx.get(self.url)
+
+ if response.status_code != HTTPStatus.OK:
+ raise HTTPRequestError(f"Error during request. {response.status_code}: {response.reason_phrase}")
+
+ return response
+
+ def get_data(self) -> dict[str, Any] | None:
+ """Asynchronous method for getting data from API."""
+ try:
+ response = self._do_req()
+ return from_json(response.read())
+ except (httpx.RequestError, HTTPRequestError, ValueError) as ex:
+ logger.exception("Request to %s failed", self.url, exc_info=ex)
+ return None
diff --git a/src/core/__init__.py b/src/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/core/cities.json b/src/core/cities.json
new file mode 100644
index 0000000..324940d
--- /dev/null
+++ b/src/core/cities.json
@@ -0,0 +1,20 @@
+{
+ "ABUDHABI": "https://code.s3.yandex.net/async-module/abudhabi-response.json",
+ "BEIJING": "https://code.s3.yandex.net/async-module/beijing-response.json",
+ "BERLIN": "https://code.s3.yandex.net/async-module/berlin-response.json",
+ "BUCHAREST": "https://code.s3.yandex.net/async-module/bucharest-response.json",
+ "CAIRO": "https://code.s3.yandex.net/async-module/cairo-response.json",
+ "GIZA": "https://code.s3.yandex.net/async-module/giza-response.json",
+ "KALININGRAD": "https://code.s3.yandex.net/async-module/kaliningrad-response.json",
+ "KAZAN": "https://code.s3.yandex.net/async-module/kazan-response.json",
+ "LONDON": "https://code.s3.yandex.net/async-module/london-response.json",
+ "MADRID": "https://code.s3.yandex.net/async-module/madrid-response.json",
+ "MOSCOW": "https://code.s3.yandex.net/async-module/moscow-response.json",
+ "NOVOSIBIRSK": "https://code.s3.yandex.net/async-module/novosibirsk-response.json",
+ "PARIS": "https://code.s3.yandex.net/async-module/paris-response.json",
+ "ROMA": "https://code.s3.yandex.net/async-module/roma-response.json",
+ "SPETERSBURG": "https://code.s3.yandex.net/async-module/spetersburg-response.json",
+ "TORONTO": "https://code.s3.yandex.net/async-module/toronto-response.json",
+ "VOLGOGRAD": "https://code.s3.yandex.net/async-module/volgograd-response.json",
+ "WARSZAWA": "https://code.s3.yandex.net/async-module/warszawa-response.json"
+}
diff --git a/src/core/exceptions.py b/src/core/exceptions.py
new file mode 100644
index 0000000..3f7b3ab
--- /dev/null
+++ b/src/core/exceptions.py
@@ -0,0 +1,10 @@
+class BaseError(Exception): ...
+
+
+class CityNotFoundError(BaseError): ...
+
+
+class UnsupportedPythonVersionError(BaseError): ...
+
+
+class HTTPRequestError(BaseError): ...
diff --git a/src/core/logger.py b/src/core/logger.py
new file mode 100644
index 0000000..b310d3c
--- /dev/null
+++ b/src/core/logger.py
@@ -0,0 +1,31 @@
+import logging
+
+from logging import config
+
+LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "formatters": {"verbose": {"format": LOG_FORMAT}},
+ "handlers": {
+ "console": {
+ "class": "logging.StreamHandler",
+ "formatter": "verbose",
+ "level": "DEBUG",
+ "stream": "ext://sys.stdout",
+ }
+ },
+ "root": {"handlers": ["console"], "level": "INFO"},
+}
+
+
+def setup_logging() -> None:
+ config.dictConfig(LOGGING)
+
+
+setup_logging()
+
+
+def get_logger(name: str) -> logging.Logger:
+ return logging.getLogger(name)
diff --git a/src/core/settings.py b/src/core/settings.py
new file mode 100644
index 0000000..41077bd
--- /dev/null
+++ b/src/core/settings.py
@@ -0,0 +1,47 @@
+from dotenv import find_dotenv, load_dotenv
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+load_dotenv(find_dotenv())
+
+
+class DefaultSettings(BaseSettings):
+ """Class to store default project settings."""
+
+ model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
+
+
+class PythonVersionSettings(DefaultSettings):
+ """Class to store Python version settings."""
+
+ min_major: int = Field(..., description="Minimum major version")
+ min_minor: int = Field(..., description="Minimum minor version")
+
+ model_config = SettingsConfigDict(env_prefix="PYTHON_VER_")
+
+
+class InputSettings(DefaultSettings):
+ day_hours_start: int = Field(..., description="Day hours start")
+ day_hours_end: int = Field(..., description="Day hours end")
+ hour_conditions_score: str = Field(..., description="Hour conditions score")
+ links_path: str = Field(..., description="Links path")
+ top_locations_count: int = Field(..., description="Top locations count")
+ cond_weight: float = Field(..., description="Condition weights")
+ temp_weight: float = Field(..., description="Temperature weights")
+
+ model_config = SettingsConfigDict(env_prefix="INPUT_")
+
+
+class OutputSettings(DefaultSettings):
+ path: str = Field(..., description="Output path")
+
+ model_config = SettingsConfigDict(env_prefix="OUTPUT_")
+
+
+class Settings(BaseSettings):
+ py_ver: PythonVersionSettings = PythonVersionSettings()
+ inpt: InputSettings = InputSettings()
+ otpt: OutputSettings = OutputSettings()
+
+
+settings = Settings()
diff --git a/src/helpers/__init__.py b/src/helpers/__init__.py
new file mode 100644
index 0000000..54658a9
--- /dev/null
+++ b/src/helpers/__init__.py
@@ -0,0 +1,4 @@
+from src.helpers.check_python_version import requires_python_version
+from src.helpers.get_json_data import from_json_file
+
+__all__: list[str] = ["requires_python_version", "from_json_file"]
diff --git a/src/helpers/check_python_version.py b/src/helpers/check_python_version.py
new file mode 100644
index 0000000..622b52a
--- /dev/null
+++ b/src/helpers/check_python_version.py
@@ -0,0 +1,29 @@
+from typing import Any, TypeVar, cast
+
+import sys
+
+from collections.abc import Callable
+from functools import wraps
+
+from src.core.exceptions import UnsupportedPythonVersionError
+from src.core.settings import settings
+
+F = TypeVar("F", bound=Callable[..., Any])
+
+
+def requires_python_version() -> Callable[[F], F]:
+ min_major = settings.py_ver.min_major
+ min_minor = settings.py_ver.min_minor
+
+ def decorator(func: F) -> F:
+ @wraps(func)
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
+ if sys.version_info.major < min_major or (
+ sys.version_info.major == min_major and sys.version_info.minor < min_minor
+ ):
+ raise UnsupportedPythonVersionError(f"Required python version >= {min_major}.{min_minor}.")
+ return func(*args, **kwargs)
+
+ return cast(F, wrapper)
+
+ return decorator
diff --git a/src/helpers/executor_manager.py b/src/helpers/executor_manager.py
new file mode 100644
index 0000000..1133fbb
--- /dev/null
+++ b/src/helpers/executor_manager.py
@@ -0,0 +1,42 @@
+from typing import Any
+
+import os
+
+from collections.abc import Generator
+from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed, wait
+
+
+class ExecutorManager:
+ """Manages executor and processing of futures."""
+
+ def __init__(self, executor_type: str = "thread") -> None:
+ self.cpu_count = os.cpu_count() or 2
+ self.executor_type = executor_type
+ (self.executor_class, self.num_workers) = self._select_executor()
+
+ def _select_executor(self) -> tuple[type[ProcessPoolExecutor | ThreadPoolExecutor], int]:
+ """Select the appropriate executor and determine the number of workers."""
+ if self.executor_type == "process":
+ return ProcessPoolExecutor, self.cpu_count - 1
+ return ThreadPoolExecutor, self.cpu_count * 2 - 1
+
+ def execute_tasks(
+ self, tasks: Generator[Any, None, None], process_func: Any, mode: str = "as_completed"
+ ) -> Generator[Any, None, None]:
+ """Execute the tasks using the selected executor and mode."""
+
+ with self.executor_class(max_workers=self.num_workers) as executor:
+ futures = [executor.submit(process_func, task) for task in tasks]
+
+ futures_iter: Any
+
+ if mode == "wait":
+ done, _ = wait(futures)
+ futures_iter = done
+ else:
+ futures_iter = as_completed(futures)
+
+ for future in futures_iter:
+ result = future.result()
+ if result is not None:
+ yield result
diff --git a/src/helpers/get_json_data.py b/src/helpers/get_json_data.py
new file mode 100644
index 0000000..5514916
--- /dev/null
+++ b/src/helpers/get_json_data.py
@@ -0,0 +1,10 @@
+from typing import Any
+
+from pathlib import Path
+
+from pydantic_core import from_json
+
+
+def from_json_file(path: str) -> dict[str, Any]:
+ with Path(path).open("rb") as file:
+ return from_json(file.read())
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..d154bd1
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,16 @@
+from src.helpers import requires_python_version
+from src.pipeline import AnalyzeTask, ExtractTask, FetchTask, TransformTask
+from src.schemas import generate_links
+
+
+@requires_python_version()
+def main() -> None:
+ links = generate_links()
+ fetched_data = FetchTask(links).run()
+ extracted_data = ExtractTask(fetched_data).run()
+ transformed_data = TransformTask(extracted_data).run(mode="wait")
+ AnalyzeTask(transformed_data).run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pipeline/__init__.py b/src/pipeline/__init__.py
new file mode 100644
index 0000000..4ea3f85
--- /dev/null
+++ b/src/pipeline/__init__.py
@@ -0,0 +1,6 @@
+from src.pipeline.analyze import AnalyzeTask
+from src.pipeline.extract import ExtractTask
+from src.pipeline.fetch import FetchTask
+from src.pipeline.transform import TransformTask
+
+__all__: list[str] = ["FetchTask", "TransformTask", "AnalyzeTask", "ExtractTask"]
diff --git a/src/pipeline/analyze.py b/src/pipeline/analyze.py
new file mode 100644
index 0000000..fa88892
--- /dev/null
+++ b/src/pipeline/analyze.py
@@ -0,0 +1,69 @@
+from typing import Any
+
+from collections.abc import Generator
+
+import polars as pl
+
+from polars import Expr
+
+from src.core.logger import get_logger
+from src.core.settings import settings as s
+from src.pipeline.base import BaseTask
+
+logger = get_logger("AnalyzeTask")
+
+
+class AnalyzeTask(BaseTask):
+ """Analyze transformed data."""
+
+ def __init__(self, data_in: Generator[dict[str, Any], None, None]) -> None:
+ super().__init__(data_in)
+
+ @staticmethod
+ def _normalize_column(df: pl.DataFrame, column_name: str) -> pl.Series:
+ """Normalize values in a column to the range [0, 1]."""
+ column_series = df.get_column(column_name)
+
+ if column_series.is_empty():
+ return column_series
+
+ if not column_series.dtype.is_numeric():
+ raise ValueError("Column must be of numeric type")
+
+ return (column_series - column_series.mean()) / column_series.std()
+
+ @staticmethod
+ def _get_weighted_sum(col1: str, col2: str, w1: float, w2: float) -> Expr:
+ """Calculate the weighted sum of two columns."""
+ return (pl.col(col1) * w1).fill_null(0) + (pl.col(col2) * w2).fill_null(0)
+
+ def process(self, item: Any | None = None) -> pl.DataFrame: # noqa:ARG002
+ """Calculate scores for each location based on the transformed data."""
+
+ transformed_data = (pl.DataFrame(item, infer_schema_length=100) for item in self.data_in)
+ pr_df = pl.concat(transformed_data, how="vertical")
+
+ pr_df = pr_df.with_columns(
+ [
+ self._normalize_column(pr_df, "temp_avg").alias("norm_temp_avg"),
+ self._normalize_column(pr_df, "cond_score").alias("norm_cond_score"),
+ ]
+ )
+
+ pr_df = pr_df.with_columns(
+ [
+ self._get_weighted_sum(
+ "norm_temp_avg", "norm_cond_score", s.inpt.temp_weight, s.inpt.cond_weight
+ ).alias("weighted_sum")
+ ]
+ )
+ return pr_df.group_by("location").agg([pl.sum("weighted_sum").alias("score")])
+
+ def run(self, mode: str | None = None) -> Any: # noqa:ARG002
+ top_locations = self.process()
+ top_locations = top_locations.sort("score", descending=True).head(s.inpt.top_locations_count)
+
+ top_locations.write_json(s.otpt.path)
+
+ logger.info("Process finished, result saved to %s", s.otpt.path)
+ logger.info("Top locations: %s", top_locations)
diff --git a/src/pipeline/base.py b/src/pipeline/base.py
new file mode 100644
index 0000000..b5a5758
--- /dev/null
+++ b/src/pipeline/base.py
@@ -0,0 +1,36 @@
+from typing import Any
+
+from abc import ABC, abstractmethod
+from collections.abc import Generator
+
+from src.helpers.executor_manager import ExecutorManager
+
+
+class BaseTask(ABC):
+ """Base class for all tasks."""
+
+ __slots__ = ("data_in", "executor_manager")
+
+ def __init__(self, data_in: Generator[Any, None, None], executor_type: str = "thread") -> None:
+ self.data_in = data_in
+ self.executor_manager = ExecutorManager(executor_type)
+
+ @abstractmethod
+ def process(self, item: Any) -> Any:
+ """Method for processing item of the iterable.
+ Args:
+ item (Any): Item of the iterable.
+ Returns:
+ Any: Processed item.
+ """
+ raise NotImplementedError("Process method must be implemented")
+
+ def run(self, mode: str = "as_completed") -> Generator[Any, None, None]:
+ """
+ Method for running task.
+ Args:
+ mode (str): Mode of the executor. "as_completed" or "wait" (default: "as_completed").
+ Returns:
+ Generator[Any, None, None]: Generator of processed items.
+ """
+ yield from self.executor_manager.execute_tasks(self.data_in, self.process, mode)
diff --git a/src/pipeline/extract.py b/src/pipeline/extract.py
new file mode 100644
index 0000000..5b04162
--- /dev/null
+++ b/src/pipeline/extract.py
@@ -0,0 +1,35 @@
+from typing import Any
+
+from collections.abc import Generator
+
+from src.core.logger import get_logger
+from src.pipeline.base import BaseTask
+from src.schemas.dto import DayDTO, ExtractTaskDTO, HourDTO
+from src.schemas.weather_response import WeatherResponse
+
+logger = get_logger("ExtractTask")
+
+
+class ExtractTask(BaseTask):
+ """Extract required data from WeatherResponse objects."""
+
+ def __init__(self, data_in: Generator[dict[str, Any], None, None]) -> None:
+ super().__init__(data_in)
+
+ def process(self, item: Any) -> dict[str, Any]:
+ item = WeatherResponse(**item)
+ days = [
+ DayDTO(
+ date=forecast.date,
+ hours=[
+ HourDTO(hour=int(hour.hour), temp=hour.temp, cond_score=hour.condition.score)
+ for hour in forecast.hours
+ if hour.is_focus
+ ],
+ )
+ for forecast in item.forecasts
+ ]
+
+ location = item.geo_object.locality.name
+ logger.info("Extracted data for %s", location)
+ return ExtractTaskDTO(location=location, days=days).model_dump()
diff --git a/src/pipeline/fetch.py b/src/pipeline/fetch.py
new file mode 100644
index 0000000..244d764
--- /dev/null
+++ b/src/pipeline/fetch.py
@@ -0,0 +1,32 @@
+from typing import Any
+
+from collections.abc import Generator
+
+from src.clients.yandex_weather import YandexWeatherAPI
+from src.core.logger import get_logger
+from src.pipeline.base import BaseTask
+from src.schemas.weather_response import WeatherResponse
+
+logger = get_logger("FetchTask")
+
+
+class FetchTask(BaseTask):
+ """Fetch data from the API."""
+
+ def __init__(self, data_in: Generator[str, None, None]) -> None:
+ super().__init__(data_in)
+
+ def process(self, item: str) -> dict[str, Any] | None:
+ api = YandexWeatherAPI(item)
+ data = api.get_data()
+
+ if data is None:
+ logger.error("Data not found for city: %s", item)
+ return None
+ try:
+ validated_data = WeatherResponse(**data)
+ logger.info("Fetched data for %s", validated_data.geo_object.locality.name)
+ return validated_data.model_dump()
+ except ValueError:
+ logger.exception("Data not valid for city: %s", item)
+ return None
diff --git a/src/pipeline/transform.py b/src/pipeline/transform.py
new file mode 100644
index 0000000..7190286
--- /dev/null
+++ b/src/pipeline/transform.py
@@ -0,0 +1,49 @@
+from typing import Any
+
+from collections.abc import Generator
+
+import polars as pl
+
+from src.core.logger import get_logger
+from src.pipeline.base import BaseTask
+from src.schemas.dto import ExtractTaskDTO, TransformedDayDTO, TransformTaskDTO
+
+logger = get_logger("TransformTask")
+
+
+class TransformTask(BaseTask):
+ """Transform extracted data."""
+
+ def __init__(self, data_in: Generator[dict[str, Any], None, None]) -> None:
+ super().__init__(data_in)
+
+ @staticmethod
+ def _transform(item: TransformTaskDTO) -> dict[str, Any]:
+ """
+ Transform data to dataframe.
+ Args:
+ item (TransformTaskDTO): Data to transform.
+ Returns:
+ dict[str, Any]: Transformed data.
+ """
+ location_df = pl.DataFrame({"location": [item.location] * len(item.days)})
+ days_df = pl.DataFrame([day.model_dump() for day in item.days])
+ result = pl.concat([location_df, days_df], how="horizontal")
+
+ return result.to_dict()
+
+ def process(self, item: Any) -> dict[str, Any]:
+ item = ExtractTaskDTO(**item)
+ days = [
+ TransformedDayDTO(
+ date=day.date,
+ hours_count=len(day.hours),
+ cond_score=sum(hour.cond_score for hour in day.hours),
+ temp_avg=sum(hour.temp for hour in day.hours) / (len(day.hours) or 1),
+ )
+ for day in item.days
+ ]
+ transform_dto = TransformTaskDTO(location=item.location, days=days)
+ result = self._transform(transform_dto)
+ logger.info("Transformed data for %s", item.location)
+ return result
diff --git a/src/schemas/__init__.py b/src/schemas/__init__.py
new file mode 100644
index 0000000..db736a1
--- /dev/null
+++ b/src/schemas/__init__.py
@@ -0,0 +1,3 @@
+from src.schemas.city import generate_links
+
+__all__: list[str] = ["generate_links"]
diff --git a/src/schemas/city.py b/src/schemas/city.py
new file mode 100644
index 0000000..41696ed
--- /dev/null
+++ b/src/schemas/city.py
@@ -0,0 +1,17 @@
+from collections.abc import Generator
+
+from pydantic import BaseModel, HttpUrl
+
+from src.core.settings import settings as s
+from src.helpers.get_json_data import from_json_file
+
+
+class City(BaseModel):
+ name: str
+ url: HttpUrl
+
+
+def generate_links() -> Generator[str, None, None]:
+ links = from_json_file(s.inpt.links_path)
+ for name, url in links.items():
+ yield City(name=name, url=url).url.unicode_string()
diff --git a/src/schemas/condition.py b/src/schemas/condition.py
new file mode 100644
index 0000000..eb0a14e
--- /dev/null
+++ b/src/schemas/condition.py
@@ -0,0 +1,33 @@
+from ast import literal_eval
+from enum import StrEnum, auto
+
+from src.core.settings import settings as s
+
+score = literal_eval(s.inpt.hour_conditions_score)
+
+
+class Condition(StrEnum):
+ clear = auto()
+ partly_cloudy = "partly-cloudy"
+ cloudy = auto()
+ overcast = auto()
+ drizzle = auto()
+ light_rain = "light-rain"
+ light_snow = "light-snow"
+ wet_snow = "wet-snow"
+ rain = auto()
+ showers = auto()
+ snow = auto()
+ snow_showers = "snow-showers"
+ moderate_rain = "moderate-rain"
+ heavy_rain = "heavy-rain"
+ continuous_heavy_rain = "continuous-heavy-rain"
+ hail = auto()
+ thunderstorm = auto()
+ thunderstorm_with_rain = "thunderstorm-with-rain"
+ thunderstorm_with_hail = "thunderstorm-with-hail"
+ unknown = auto()
+
+ @property
+ def score(self) -> int:
+ return score.get(self.name, 0)
diff --git a/src/schemas/dto.py b/src/schemas/dto.py
new file mode 100644
index 0000000..b0b63a1
--- /dev/null
+++ b/src/schemas/dto.py
@@ -0,0 +1,31 @@
+from pydantic import BaseModel
+
+from src.schemas.mixins import DateMixin
+
+
+class CommonDTO(BaseModel):
+ location: str
+
+
+class HourDTO(BaseModel):
+ hour: int
+ temp: int
+ cond_score: int
+
+
+class DayDTO(DateMixin):
+ hours: list[HourDTO]
+
+
+class ExtractTaskDTO(CommonDTO):
+ days: list[DayDTO]
+
+
+class TransformedDayDTO(DateMixin):
+ hours_count: int
+ cond_score: int
+ temp_avg: float
+
+
+class TransformTaskDTO(CommonDTO):
+ days: list[TransformedDayDTO]
diff --git a/src/schemas/mixins.py b/src/schemas/mixins.py
new file mode 100644
index 0000000..631d95d
--- /dev/null
+++ b/src/schemas/mixins.py
@@ -0,0 +1,28 @@
+from pydantic import BaseModel, Field
+
+from src.schemas.condition import Condition
+
+
+class WeatherDataMixin(BaseModel):
+ temp: int = Field(lt=80, description="Temperature")
+ feels_like: int
+ icon: str
+ condition: Condition = Field(default=Condition.unknown, description="Condition with default unknown")
+ cloudness: float
+ prec_type: int
+ prec_strength: float
+ prec_prob: int
+ is_thunder: bool
+ wind_speed: float
+ wind_dir: str
+ pressure_mm: int
+ pressure_pa: int
+ humidity: int
+ uv_index: int
+ soil_temp: int | None = None
+ soil_moisture: float | None = None
+ wind_gust: float | None = None
+
+
+class DateMixin(BaseModel):
+ date: str
diff --git a/src/schemas/weather_response.py b/src/schemas/weather_response.py
new file mode 100644
index 0000000..7611884
--- /dev/null
+++ b/src/schemas/weather_response.py
@@ -0,0 +1,95 @@
+from pydantic import BaseModel
+
+from src.core.settings import settings as s
+from src.schemas.mixins import WeatherDataMixin
+
+
+class Tzinfo(BaseModel):
+ name: str
+ abbr: str
+ dst: bool
+ offset: int
+
+
+class Info(BaseModel):
+ n: bool
+ geoid: int
+ url: str
+ lat: float
+ lon: float
+ tzinfo: Tzinfo
+ def_pressure_mm: int
+ def_pressure_pa: int
+ slug: str
+ zoom: int
+ nr: bool
+ ns: bool
+ nsr: bool
+ p: bool
+ f: bool
+ _h: bool
+
+
+class Location(BaseModel):
+ id: int
+ name: str
+
+
+class GeoObject(BaseModel):
+ district: Location | None = None
+ locality: Location
+ province: Location | None = None
+ country: Location | None = None
+
+
+class Yesterday(BaseModel):
+ temp: int
+
+
+class Fact(WeatherDataMixin):
+ obs_time: int
+ uptime: int
+ daytime: str
+ polar: bool
+ season: str
+ source: str
+
+
+class Hour(WeatherDataMixin):
+ hour: str
+ hour_ts: int
+ prec_mm: float
+ prec_period: int
+
+ @property
+ def is_focus(self) -> bool:
+ return bool(s.inpt.day_hours_start <= int(self.hour) and s.inpt.day_hours_end)
+
+
+class Biomet(BaseModel):
+ index: int
+ condition: str
+
+
+class Forecast(BaseModel):
+ date: str
+ date_ts: int
+ week: int
+ sunrise: str
+ sunset: str
+ rise_begin: str
+ set_end: str
+ moon_code: int
+ moon_text: str
+ hours: list[Hour]
+ biomet: Biomet | None = None
+
+
+class WeatherResponse(BaseModel):
+ now: int
+ now_dt: str
+ info: Info
+ geo_object: GeoObject
+ yesterday: Yesterday
+ fact: Fact | None = None
+ forecasts: list[Forecast]
diff --git a/tasks.py b/tasks.py
deleted file mode 100644
index f7f8645..0000000
--- a/tasks.py
+++ /dev/null
@@ -1,14 +0,0 @@
-class DataFetchingTask:
- pass
-
-
-class DataCalculationTask:
- pass
-
-
-class DataAggregationTask:
- pass
-
-
-class DataAnalyzingTask:
- pass
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_analyze.py b/tests/test_analyze.py
new file mode 100644
index 0000000..d592f7c
--- /dev/null
+++ b/tests/test_analyze.py
@@ -0,0 +1,55 @@
+import logging
+
+import pytest
+
+from src.pipeline.analyze import AnalyzeTask
+from src.schemas.dto import TransformedDayDTO, TransformTaskDTO
+
+
+@pytest.fixture
+def mock_transformed_data():
+ return [
+ TransformTaskDTO(
+ location="Location A",
+ days=[TransformedDayDTO(date="2024-10-10", hours_count=10, cond_score=9, temp_avg=20)],
+ ),
+ TransformTaskDTO(
+ location="Location B",
+ days=[TransformedDayDTO(date="2024-10-10", hours_count=10, cond_score=8, temp_avg=18)],
+ ),
+ TransformTaskDTO(
+ location="Location C",
+ days=[TransformedDayDTO(date="2024-10-10", hours_count=10, cond_score=7, temp_avg=15)],
+ ),
+ TransformTaskDTO(
+ location="Location D",
+ days=[TransformedDayDTO(date="2024-10-10", hours_count=10, cond_score=5, temp_avg=10)],
+ ),
+ ]
+
+
+def test_analyze_task_top_3_locations(mock_transformed_data, mocker, caplog):
+ mocker.patch("src.core.settings.settings.inpt.temp_weight", 0.7)
+ mocker.patch("src.core.settings.settings.inpt.cond_weight", 0.3)
+ mocker.patch("src.core.settings.settings.inpt.top_locations_count", 3)
+ mocker.patch("src.core.settings.settings.otpt.path", "/mocked_path")
+
+ mocker.patch("polars.DataFrame.write_json")
+
+ # Preparing the mock data
+ data_in = (
+ {"location": dto.location, "temp_avg": day.temp_avg, "cond_score": day.cond_score}
+ for dto in mock_transformed_data
+ for day in dto.days
+ )
+
+ with caplog.at_level(logging.INFO):
+ analyze_task = AnalyzeTask(data_in=data_in)
+ analyze_task.run()
+
+ log_output = [record.message for record in caplog.records if "Top locations:" in record.message]
+ assert len(log_output) > 0, "Expected top locations to be logged but none found."
+
+ expected_top_3 = ["Location A", "Location B", "Location C"]
+ for loc in expected_top_3:
+ assert loc in log_output[0], f"Expected {loc} in top locations, but not found."
diff --git a/tests/test_fetch.py b/tests/test_fetch.py
new file mode 100644
index 0000000..502d692
--- /dev/null
+++ b/tests/test_fetch.py
@@ -0,0 +1,65 @@
+import pytest
+
+from polyfactory.factories.pydantic_factory import ModelFactory
+
+from src.clients.yandex_weather import YandexWeatherAPI
+from src.pipeline.fetch import FetchTask
+from src.schemas.weather_response import WeatherResponse
+
+
+class WeatherResponseFactory(ModelFactory[WeatherResponse]):
+ __random_seed__ = 1
+
+
+@pytest.fixture
+def weather_response():
+ return WeatherResponseFactory.build()
+
+
+@pytest.fixture
+def mock_invalid_data():
+ return {"geo_object": {"locality": {"name": "Test City"}}, "fact": {"temp": "invalid_temperature"}}
+
+
+@pytest.fixture
+def mock_missing_forecast():
+ return {"geo_object": {"locality": {"name": "Test City"}}, "fact": {"temp": 20}}
+
+
+def test_fetch_task_invalid_url(mocker):
+ mocker.patch.object(YandexWeatherAPI, "get_data", return_value=None)
+ fetch_task = FetchTask(data_in=(city for city in ["http://invalid-url.com"]))
+ result = fetch_task.process("http://invalid-url.com")
+ assert result is None
+
+
+def test_fetch_task_no_data(mocker):
+ mocker.patch.object(YandexWeatherAPI, "get_data", return_value=None)
+ fetch_task = FetchTask(data_in=(city for city in ["http://no-data-url.com"]))
+ result = fetch_task.process("http://no-data-url.com")
+ assert result is None
+
+
+def test_fetch_task_invalid_data(mock_invalid_data, mocker):
+ mocker.patch.object(YandexWeatherAPI, "get_data", return_value=mock_invalid_data)
+ fetch_task = FetchTask(data_in=(city for city in ["http://invalid-data-url.com"]))
+ result = fetch_task.process("http://invalid-data-url.com")
+ assert result is None
+
+
+def test_fetch_task_missing_forecast(mock_missing_forecast, mocker):
+ mocker.patch.object(YandexWeatherAPI, "get_data", return_value=mock_missing_forecast)
+ fetch_task = FetchTask(data_in=(city for city in ["http://missing-forecast-url.com"]))
+ result = fetch_task.process("http://missing-forecast-url.com")
+ assert result is None
+
+
+def test_fetch_task_success(weather_response, mocker):
+ mocker.patch.object(YandexWeatherAPI, "get_data", return_value=weather_response.model_dump())
+
+ fetch_task = FetchTask(data_in=(city for city in ["http://valid-url.com"]))
+ result = fetch_task.process("http://valid-url.com")
+
+ assert result is not None
+ assert result["geo_object"]["locality"]["name"] == weather_response.geo_object.locality.name
+ assert result["fact"]["temp"] == weather_response.fact.temp
diff --git a/utils.py b/utils.py
deleted file mode 100644
index 2e0fe71..0000000
--- a/utils.py
+++ /dev/null
@@ -1,45 +0,0 @@
-CITIES = {
- "MOSCOW": "https://code.s3.yandex.net/async-module/moscow-response.json",
- "PARIS": "https://code.s3.yandex.net/async-module/paris-response.json",
- "LONDON": "https://code.s3.yandex.net/async-module/london-response.json",
- "BERLIN": "https://code.s3.yandex.net/async-module/berlin-response.json",
- "BEIJING": "https://code.s3.yandex.net/async-module/beijing-response.json",
- "KAZAN": "https://code.s3.yandex.net/async-module/kazan-response.json",
- "SPETERSBURG": "https://code.s3.yandex.net/async-module/spetersburg-response.json",
- "VOLGOGRAD": "https://code.s3.yandex.net/async-module/volgograd-response.json",
- "NOVOSIBIRSK": "https://code.s3.yandex.net/async-module/novosibirsk-response.json",
- "KALININGRAD": "https://code.s3.yandex.net/async-module/kaliningrad-response.json",
- "ABUDHABI": "https://code.s3.yandex.net/async-module/abudhabi-response.json",
- "WARSZAWA": "https://code.s3.yandex.net/async-module/warszawa-response.json",
- "BUCHAREST": "https://code.s3.yandex.net/async-module/bucharest-response.json",
- "ROMA": "https://code.s3.yandex.net/async-module/roma-response.json",
- "CAIRO": "https://code.s3.yandex.net/async-module/cairo-response.json",
-
- "GIZA": "https://code.s3.yandex.net/async-module/giza-response.json",
- "MADRID": "https://code.s3.yandex.net/async-module/madrid-response.json",
- "TORONTO": "https://code.s3.yandex.net/async-module/toronto-response.json"
-}
-
-MIN_MAJOR_PYTHON_VER = 3
-MIN_MINOR_PYTHON_VER = 9
-
-
-def check_python_version():
- import sys
-
- if (
- sys.version_info.major < MIN_MAJOR_PYTHON_VER
- or sys.version_info.minor < MIN_MINOR_PYTHON_VER
- ):
- raise Exception(
- "Please use python version >= {}.{}".format(
- MIN_MAJOR_PYTHON_VER, MIN_MINOR_PYTHON_VER
- )
- )
-
-
-def get_url_by_city_name(city_name):
- try:
- return CITIES[city_name]
- except KeyError:
- raise Exception("Please check that city {} exists".format(city_name))
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..76f1c09
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,527 @@
+version = 1
+requires-python = ">=3.13"
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
+]
+
+[[package]]
+name = "anyio"
+version = "4.6.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/49/f3f17ec11c4a91fe79275c426658e509b07547f874b14c1a526d86a83fc8/anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb", size = 170983 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a", size = 89631 },
+]
+
+[[package]]
+name = "async-python-sprint-1"
+version = "0.5.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "httpx" },
+ { name = "polars" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "coverage" },
+ { name = "mypy" },
+ { name = "polyfactory" },
+ { name = "pre-commit" },
+ { name = "pyright" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "pytest-mock" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "httpx", specifier = ">=0.27.2" },
+ { name = "polars", specifier = ">=1.9.0" },
+ { name = "pydantic", specifier = ">=2.9.2" },
+ { name = "pydantic-settings", specifier = ">=2.5.2" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "coverage", specifier = ">=7.6.1" },
+ { name = "mypy", specifier = ">=1.11.2" },
+ { name = "polyfactory", specifier = ">=2.17.0" },
+ { name = "pre-commit", specifier = ">=3.8.0" },
+ { name = "pyright", specifier = ">=1.1.384" },
+ { name = "pytest", specifier = ">=8.3.3" },
+ { name = "pytest-cov", specifier = ">=5.0.0" },
+ { name = "pytest-mock", specifier = ">=3.14.0" },
+ { name = "ruff", specifier = ">=0.6.9" },
+]
+
+[[package]]
+name = "certifi"
+version = "2024.8.30"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/ee/9b19140fe824b367c04c5e1b369942dd754c4c5462d5674002f75c4dedc1/certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9", size = 168507 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/90/3c9ff0512038035f59d279fddeb79f5f1eccd8859f06d6163c58798b9487/certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", size = 167321 },
+]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
+]
+
+[[package]]
+name = "coverage"
+version = "7.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007 },
+ { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269 },
+ { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886 },
+ { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037 },
+ { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038 },
+ { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690 },
+ { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765 },
+ { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611 },
+ { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671 },
+ { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368 },
+ { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758 },
+ { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035 },
+ { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839 },
+ { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569 },
+ { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927 },
+ { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401 },
+ { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301 },
+ { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598 },
+ { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307 },
+ { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453 },
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c4/91/e2df406fb4efacdf46871c25cde65d3c6ee5e173b7e5a4547a47bae91920/distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64", size = 609931 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/41/9307e4f5f9976bc8b7fea0b66367734e8faf3ec84bc0d412d8cfabbb66cd/distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784", size = 468850 },
+]
+
+[[package]]
+name = "faker"
+version = "30.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/f4/a6fe96b1bc0a8b864ddcd087e7909d8c6ccb74a1247baadbf5c7094a9067/faker-30.1.0.tar.gz", hash = "sha256:e0593931bd7be9a9ea984b5d8c302ef1cec19392585d1e90d444199271d0a94d", size = 1796615 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/61/49/34f3615d1f4770f70cda4df1ce09d1203cf66b62baabac307c28887c69af/Faker-30.1.0-py3-none-any.whl", hash = "sha256:dbf81295c948270a9e96cd48a9a3ebec73acac9a153d0c854fbbd0294557609f", size = 1836618 },
+]
+
+[[package]]
+name = "filelock"
+version = "3.16.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
+]
+
+[[package]]
+name = "h11"
+version = "0.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 },
+]
+
+[[package]]
+name = "httpx"
+version = "0.27.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+ { name = "sniffio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
+]
+
+[[package]]
+name = "identify"
+version = "2.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972 },
+]
+
+[[package]]
+name = "idna"
+version = "3.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
+]
+
+[[package]]
+name = "mypy"
+version = "1.11.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mypy-extensions" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5c/86/5d7cbc4974fd564550b80fbb8103c05501ea11aa7835edf3351d90095896/mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", size = 3078806 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/3a/bdf730640ac523229dd6578e8a581795720a9321399de494374afc437ec5/mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", size = 2619625 },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
+]
+
+[[package]]
+name = "packaging"
+version = "24.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/51/65/50db4dda066951078f0a96cf12f4b9ada6e4b811516bf0262c0f4f7064d4/packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", size = 148788 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985 },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.3.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
+]
+
+[[package]]
+name = "polars"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/09/c2fb0b231d551e0c8e68097d08577712bdff1ba91346cda8228e769602f5/polars-1.9.0.tar.gz", hash = "sha256:8e1206ef876f61c1d50a81e102611ea92ee34631cb135b46ad314bfefd3cb122", size = 4027431 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/cc/3d0292048d8f9045a03510aeecda2e6ed9df451ae8853274946ff841f98b/polars-1.9.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a471d2ce96f6fa5dd0ef16bcdb227f3dbe3af8acb776ca52f9e64ef40c7489a0", size = 31870933 },
+ { url = "https://files.pythonhosted.org/packages/ee/be/15af97f4d8b775630da16a8bf0141507d9c0ae5f2637b9a27ed337b3b1ba/polars-1.9.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94b12d731cd200d2c50b13fc070d6353f708e632bca6529c5a72aa6a69e5285d", size = 28171055 },
+ { url = "https://files.pythonhosted.org/packages/bb/57/b286b317f061d8f17bab4726a27e7b185fbf3d3db65cf689074256ea34a9/polars-1.9.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f85f132732aa63c6f3b502b0fdfc3ba9f0b78cc6330059b5a2d6f9fd78508acb", size = 33063367 },
+ { url = "https://files.pythonhosted.org/packages/e5/25/bf5d43dcb538bf6573b15f3d5995a52be61b8fbce0cd737e72c4d25eef88/polars-1.9.0-cp38-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:f753c8941a3b3249d59262d68a856714a96a7d4e16977aefbb196be0c192e151", size = 29764698 },
+ { url = "https://files.pythonhosted.org/packages/a6/cf/f9170a3ac20e0efb9d3c1cdacc677e35b711ffd5ec48a6d5f3da7b7d8663/polars-1.9.0-cp38-abi3-win_amd64.whl", hash = "sha256:95de07066cd797dd940fa2783708a7bef93c827a57be0f4dfad3575a6144212b", size = 32819142 },
+]
+
+[[package]]
+name = "polyfactory"
+version = "2.17.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "faker" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3c/74/25adfbef58c43bdd3b0afb12a98438fabdd3dcdb41c1babaca6455eed047/polyfactory-2.17.0.tar.gz", hash = "sha256:099d86f7c79c51a2caaf7c8598cc56e7b0a57c11b5918ddf699e82380735b6b7", size = 183719 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/ca/7c9b5e6270a911dd48165dd82c2e129b30dd5b8f0bf4da5379c94745c68c/polyfactory-2.17.0-py3-none-any.whl", hash = "sha256:71b677c17bb7cebad9a5631b1aca7718280bdcedc1c25278253717882d1ac294", size = 58820 },
+]
+
+[[package]]
+name = "pre-commit"
+version = "3.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cfgv" },
+ { name = "identify" },
+ { name = "nodeenv" },
+ { name = "pyyaml" },
+ { name = "virtualenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/10/97ee2fa54dff1e9da9badbc5e35d0bbaef0776271ea5907eccf64140f72f/pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", size = 177815 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/92/caae8c86e94681b42c246f0bca35c059a2f0529e5b92619f6aba4cf7e7b6/pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f", size = 204643 },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.9.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.23.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 },
+ { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 },
+ { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 },
+ { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 },
+ { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 },
+ { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 },
+ { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 },
+ { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 },
+ { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 },
+ { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 },
+ { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 },
+ { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/68/27/0bed9dd26b93328b60a1402febc780e7be72b42847fa8b5c94b7d0aeb6d1/pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0", size = 70938 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/29/8d/29e82e333f32d9e2051c10764b906c2a6cd140992910b5f49762790911ba/pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907", size = 26864 },
+]
+
+[[package]]
+name = "pyright"
+version = "1.1.384"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nodeenv" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/84/00/a23114619f9d005f4b0f35e037c76cee029174d090a6f73a355749c74f4a/pyright-1.1.384.tar.gz", hash = "sha256:25e54d61f55cbb45f1195ff89c488832d7a45d59f3e132f178fdf9ef6cafc706", size = 21956 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6d/4a/e7f4d71d194ba675f3577d11eebe4e17a592c4d1c3f9986d4b321ba3c809/pyright-1.1.384-py3-none-any.whl", hash = "sha256:f0b6f4db2da38f27aeb7035c26192f034587875f751b847e9ad42ed0c704ac9e", size = 18578 },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/6c/62bbd536103af674e227c41a8f3dcd022d591f6eed5facb5a0f31ee33bbc/pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181", size = 1442487 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2", size = 342341 },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "5.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990 },
+]
+
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
+ { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
+ { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
+ { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
+ { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
+ { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
+ { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
+ { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
+ { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
+]
+
+[[package]]
+name = "ruff"
+version = "0.6.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/0d/6148a48dab5662ca1d5a93b7c0d13c03abd3cc7e2f35db08410e47cef15d/ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2", size = 3095355 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/8f/f7a0a0ef1818662efb32ed6df16078c95da7a0a3248d64c2410c1e27799f/ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd", size = 10440526 },
+ { url = "https://files.pythonhosted.org/packages/8b/69/b179a5faf936a9e2ab45bb412a668e4661eded964ccfa19d533f29463ef6/ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec", size = 10034612 },
+ { url = "https://files.pythonhosted.org/packages/c7/ef/fd1b4be979c579d191eeac37b5cfc0ec906de72c8bcd8595e2c81bb700c1/ruff-0.6.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53fd8ca5e82bdee8da7f506d7b03a261f24cd43d090ea9db9a1dc59d9313914c", size = 9706197 },
+ { url = "https://files.pythonhosted.org/packages/29/61/b376d775deb5851cb48d893c568b511a6d3625ef2c129ad5698b64fb523c/ruff-0.6.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645d7d8761f915e48a00d4ecc3686969761df69fb561dd914a773c1a8266e14e", size = 10751855 },
+ { url = "https://files.pythonhosted.org/packages/13/d7/def9e5f446d75b9a9c19b24231a3a658c075d79163b08582e56fa5dcfa38/ruff-0.6.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eae02b700763e3847595b9d2891488989cac00214da7f845f4bcf2989007d577", size = 10200889 },
+ { url = "https://files.pythonhosted.org/packages/6c/d6/7f34160818bcb6e84ce293a5966cba368d9112ff0289b273fbb689046047/ruff-0.6.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d5ccc9e58112441de8ad4b29dcb7a86dc25c5f770e3c06a9d57e0e5eba48829", size = 11038678 },
+ { url = "https://files.pythonhosted.org/packages/13/34/a40ff8ae62fb1b26fb8e6fa7e64bc0e0a834b47317880de22edd6bfb54fb/ruff-0.6.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:417b81aa1c9b60b2f8edc463c58363075412866ae4e2b9ab0f690dc1e87ac1b5", size = 11808682 },
+ { url = "https://files.pythonhosted.org/packages/2e/6d/25a4386ae4009fc798bd10ba48c942d1b0b3e459b5403028f1214b6dd161/ruff-0.6.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c866b631f5fbce896a74a6e4383407ba7507b815ccc52bcedabb6810fdb3ef7", size = 11330446 },
+ { url = "https://files.pythonhosted.org/packages/f7/f6/bdf891a9200d692c94ebcd06ae5a2fa5894e522f2c66c2a12dd5d8cb2654/ruff-0.6.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7b118afbb3202f5911486ad52da86d1d52305b59e7ef2031cea3425142b97d6f", size = 12483048 },
+ { url = "https://files.pythonhosted.org/packages/a7/86/96f4252f41840e325b3fa6c48297e661abb9f564bd7dcc0572398c8daa42/ruff-0.6.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a67267654edc23c97335586774790cde402fb6bbdb3c2314f1fc087dee320bfa", size = 10936855 },
+ { url = "https://files.pythonhosted.org/packages/45/87/801a52d26c8dbf73424238e9908b9ceac430d903c8ef35eab1b44fcfa2bd/ruff-0.6.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3ef0cc774b00fec123f635ce5c547dac263f6ee9fb9cc83437c5904183b55ceb", size = 10713007 },
+ { url = "https://files.pythonhosted.org/packages/be/27/6f7161d90320a389695e32b6ebdbfbedde28ccbf52451e4b723d7ce744ad/ruff-0.6.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:12edd2af0c60fa61ff31cefb90aef4288ac4d372b4962c2864aeea3a1a2460c0", size = 10274594 },
+ { url = "https://files.pythonhosted.org/packages/00/52/dc311775e7b5f5b19831563cb1572ecce63e62681bccc609867711fae317/ruff-0.6.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:55bb01caeaf3a60b2b2bba07308a02fca6ab56233302406ed5245180a05c5625", size = 10608024 },
+ { url = "https://files.pythonhosted.org/packages/98/b6/be0a1ddcbac65a30c985cf7224c4fce786ba2c51e7efeb5178fe410ed3cf/ruff-0.6.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:925d26471fa24b0ce5a6cdfab1bb526fb4159952385f386bdcc643813d472039", size = 10982085 },
+ { url = "https://files.pythonhosted.org/packages/bb/a4/c84bc13d0b573cf7bb7d17b16d6d29f84267c92d79b2f478d4ce322e8e72/ruff-0.6.9-py3-none-win32.whl", hash = "sha256:eb61ec9bdb2506cffd492e05ac40e5bc6284873aceb605503d8494180d6fc84d", size = 8522088 },
+ { url = "https://files.pythonhosted.org/packages/74/be/fc352bd8ca40daae8740b54c1c3e905a7efe470d420a268cd62150248c91/ruff-0.6.9-py3-none-win_amd64.whl", hash = "sha256:785d31851c1ae91f45b3d8fe23b8ae4b5170089021fbb42402d811135f0b7117", size = 9359275 },
+ { url = "https://files.pythonhosted.org/packages/3e/14/fd026bc74ded05e2351681545a5f626e78ef831f8edce064d61acd2e6ec7/ruff-0.6.9-py3-none-win_arm64.whl", hash = "sha256:a9641e31476d601f83cd602608739a0840e348bda93fec9f1ee816f8b6798b93", size = 8679879 },
+]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053 },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.12.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.26.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "distlib" },
+ { name = "filelock" },
+ { name = "platformdirs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3f/40/abc5a766da6b0b2457f819feab8e9203cbeae29327bd241359f866a3da9d/virtualenv-20.26.6.tar.gz", hash = "sha256:280aede09a2a5c317e409a00102e7077c6432c5a38f0ef938e643805a7ad2c48", size = 9372482 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/90/57b8ac0c8a231545adc7698c64c5a36fa7cd8e376c691b9bde877269f2eb/virtualenv-20.26.6-py3-none-any.whl", hash = "sha256:7345cc5b25405607a624d8418154577459c3e0277f5466dd79c49d5e492995f2", size = 5999862 },
+]