final
This commit is contained in:
87
README.md
Normal file
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# ИИ-помощник туриста
|
||||
|
||||
Веб-сервис, который создает **персональные пешеходные маршруты**, понимая **свободную речь пользователя**. Наш AI-гид анализирует ваши желания, текущее местоположение и время, чтобы построить уникальную прогулку, которая подходит именно вам. Сервис доступен [здесь: https://gorkycode.nikidze.ru/](https://gorkycode.nikidze.ru/).
|
||||
|
||||
---
|
||||
|
||||
## Понимание контекста
|
||||
|
||||
В отличие от конкурентов, которые просто ищут по тегам, наш сервис **понимает логику и последовательность** запроса. Мы не просто находим "музей" и "кофе", мы строим маршрут так, как этого хотел пользователь.
|
||||
|
||||
Как система обработает внешне похожие запросы:
|
||||
|
||||
| Запрос пользователя | Интерпретация AI и результат |
|
||||
| :--- | :--- |
|
||||
| *«Хочу сходить в музей и попить кофе»* | Найдет музей и кофейню рядом. Порядок **не важен**. |
|
||||
| *«Хочу попить кофе, потом в музей»* | Построит маршрут **сначала в кофейню**, а уже **потом в музей**. |
|
||||
| *«Хочу походить по музеям, потом зайти за кофе»* | Включит в маршрут **несколько музеев**, а кофейня будет **финальной точкой**. |
|
||||
| *«Хочу в музей или попить кофе, потом на канатку»* | Выберет **одно из двух** (музей или кафе), а затем **обязательно добавит** канатную дорогу. |
|
||||
|
||||
Эта способность понимать нюансы делает наш сервис по-настоящему персональным и умным.
|
||||
|
||||
---
|
||||
|
||||
## Основные возможности
|
||||
|
||||
- **Пошаговый интерфейс** — Удобный опрос в несколько шагов, который не перегружает пользователя и позволяет легко вернуться для корректировки.
|
||||
- **Диалоговый режим** — Если данных не хватает, система задаст уточняющие вопросы, пока не получит всю необходимую информацию.
|
||||
- **Умная маршрутизация** — Алгоритм учитывает время на дорогу пешком и посещение локаций, отсекая нерелевантные точки.
|
||||
- **Гибкий анализ запроса** — AI (Gemini 2.5 Flash) извлекает теги, время и адрес, даже если они указаны неточно (например, *«свободен до 6 вечера»* или адрес с опечаткой).
|
||||
- **Микросервисная архитектура** — Проект разбит на независимые Docker-контейнеры, что обеспечивает надежность и масштабируемость.
|
||||
|
||||
---
|
||||
|
||||
## Как это работает: от запроса до маршрута
|
||||
|
||||
1. **Сбор данных (UI + AI)**
|
||||
Пользователь в свободной форме отвечает на вопросы о своих предпочтениях, времени и местоположении. Данные отправляются в основной сервис `input_to_route`.
|
||||
|
||||
2. **Анализ и структурирование (AI: Gemini 2.5 Flash)**
|
||||
С помощью модели `Gemini 2.5 Flash` через `OpenRouter API` текстовый запрос преобразуется в структурированные данные:
|
||||
```json
|
||||
{
|
||||
"tags": [["Музей"], ["Кофейня"]],
|
||||
"user_location": "улица Ошарская, дом 14",
|
||||
"time": 180
|
||||
}
|
||||
```
|
||||
На этом же этапе нормализуется адрес пользователя с помощью отдельного сервиса на базе `libpostal`.
|
||||
|
||||
3. **Поиск кандидатов (База данных)**
|
||||
Сервис находит в базе данных все объекты, соответствующие извлеченным тегам.
|
||||
|
||||
4. **Построение маршрута (OSRM)**
|
||||
- Координаты пользователя и найденных точек передаются в `navigation-engine` (кастомная сборка `OSRM` с картой Нижнего Новгорода).
|
||||
- Алгоритм отсекает слишком удаленные точки.
|
||||
- Строится матрица временных затрат на перемещение между оставшимися точками.
|
||||
- На основе матрицы и логики тегов ("и", "или", "потом") генерируется список подходящих по времени маршрутов.
|
||||
- Из списка выбирается один (на данный момент — случайный).
|
||||
|
||||
5. **Генерация описания (AI)**
|
||||
На основе выбранных точек маршрута `Gemini 2.5 Flash` генерирует красивое и связное текстовое описание прогулки для пользователя.
|
||||
|
||||
---
|
||||
|
||||
## Архитектура и технологии
|
||||
|
||||
Проект построен на основе микросервисной архитектуры и развернут в Docker-контейнерах на платформе `dockhost`. Такой подход обеспечивает изоляцию компонентов и простоту масштабирования.
|
||||
|
||||
| Компонент | Технологии | Роль |
|
||||
| :--- | :--- | :--- |
|
||||
| **web-ui** | **Vue 3, JavaScript** | Пользовательский интерфейс: пошаговый опрос и отображение результата. |
|
||||
| **input_to_route** | **FastAPI (Python)** | **Мозг системы.** Оркестрирует весь процесс: общается с UI, AI, базой данных и другими сервисами. |
|
||||
| **engine-wrapper** | **FastAPI (Python)** | Упрощенная обертка над движком OSRM для удобства взаимодействия. |
|
||||
| **navigation-engine** | **OSRM Backend (C++)** | Высокопроизводительный движок для построения пешеходных маршрутов. |
|
||||
| **libpostal-service** | **libpostal, REST API** | Сервис для нормализации и парсинга географических адресов. Вынесен в отдельный контейнер из-за большого веса библиотеки. |
|
||||
| **AI-провайдер** | **OpenRouter API (Gemini 2.5 Flash)** | Предоставляет доступ к LLM для анализа запросов и генерации текста. |
|
||||
| **Платформа** | **Dockhost** | Облачная платформа для хостинга Docker-контейнеров. |
|
||||
|
||||
---
|
||||
|
||||
## Почему наш стек эффективен
|
||||
|
||||
1. **FastAPI** — Идеально подходит для создания асинхронных API, которые связывают разные сервисы. Быстрый, современный и легок в освоении.
|
||||
2. **Vue 3** — Реактивный фреймворк, который позволяет создавать динамичные и отзывчивые пользовательские интерфейсы, как наш пошаговый опросник.
|
||||
3. **OSRM** — Один из самых быстрых open-source движков для маршрутизации. Использование кастомной сборки только для Нижнего Новгорода делает его еще более производительным.
|
||||
4. **Gemini 2.5 Flash через OpenRouter** — Экономически эффективное решение. Модель обеспечивает высокую скорость, отличное качество понимания русского языка и следование сложным инструкциям, что является ядром нашего проекта.
|
||||
5. **Микросервисы на Docker** — Стандарт индустрии. Позволяет независимо разрабатывать, обновлять и масштабировать части приложения. `libpostal` в отдельном контейнере — классический пример правильного применения этого подхода.
|
||||
3
engine-wrapper/.dockerignore
Normal file
3
engine-wrapper/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
.venv
|
||||
project.toml
|
||||
1
engine-wrapper/.env
Normal file
1
engine-wrapper/.env
Normal file
@@ -0,0 +1 @@
|
||||
OSRM_URL=https://8msn-80q0-el3y.gw-1a.dockhost.net
|
||||
10
engine-wrapper/.gitignore
vendored
Normal file
10
engine-wrapper/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Python-generated files
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
|
||||
# Virtual environments
|
||||
.venv
|
||||
1
engine-wrapper/.python-version
Normal file
1
engine-wrapper/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
22
engine-wrapper/Dockerfile
Normal file
22
engine-wrapper/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# Используем официальный образ Python
|
||||
FROM python:3.13-slim
|
||||
|
||||
# Устанавливаем рабочую директорию внутри контейнера
|
||||
WORKDIR /app
|
||||
|
||||
# Копируем файлы зависимостей
|
||||
COPY requirements.txt .
|
||||
|
||||
# Устанавливаем зависимости
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копируем остальные файлы приложения
|
||||
COPY . .
|
||||
|
||||
# Открываем порт, на котором будет слушать FastAPI
|
||||
EXPOSE 8000
|
||||
|
||||
# Запускаем приложение с помощью Uvicorn
|
||||
# --host 0.0.0.0 делает приложение доступным извне контейнера
|
||||
# --port 8000 указывает порт, на котором слушает Uvicorn
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
0
engine-wrapper/README.md
Normal file
0
engine-wrapper/README.md
Normal file
126
engine-wrapper/main.py
Normal file
126
engine-wrapper/main.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
import httpx
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Загрузка переменных окружения из .env файла
|
||||
load_dotenv()
|
||||
|
||||
app = FastAPI(title="OSRM API Wrapper")
|
||||
|
||||
# --- Модели для запросов и ответов ---
|
||||
|
||||
# Входная модель для обоих эндпоинтов: массив точек
|
||||
class PointsRequest(BaseModel):
|
||||
points: List[List[float]] # Формат: [[lat, lon], [lat, lon], ...]
|
||||
|
||||
# Модель ответа для /table
|
||||
class TableResponse(BaseModel):
|
||||
distances: List[List[float]]
|
||||
durations: List[List[float]]
|
||||
|
||||
# Модель для одного шага в маршруте (для /route)
|
||||
class RouteStep(BaseModel):
|
||||
location: List[float] # [lon, lat]
|
||||
distance: float
|
||||
type: str
|
||||
modifier: Optional[str] = ""
|
||||
name: str
|
||||
|
||||
# Модель ответа для /route
|
||||
class RouteResponse(BaseModel):
|
||||
steps: List[RouteStep]
|
||||
|
||||
# --- Конфигурация ---
|
||||
|
||||
# Чтение URL OSRM из переменной окружения
|
||||
OSRM_BASE_URL = os.getenv("OSRM_URL")
|
||||
|
||||
if not OSRM_BASE_URL:
|
||||
raise RuntimeError("Переменная окружения OSRM_URL не установлена.")
|
||||
|
||||
# --- Эндпоинты ---
|
||||
|
||||
@app.post("/table", response_model=TableResponse)
|
||||
async def table(request: PointsRequest):
|
||||
"""
|
||||
Принимает список координат и возвращает матрицы расстояний и времени в пути между ними.
|
||||
"""
|
||||
if len(request.points) < 2:
|
||||
raise HTTPException(status_code=400, detail="Нужно минимум 2 точки")
|
||||
|
||||
# OSRM ожидает формат lon,lat
|
||||
coords = ";".join(f"{lon},{lat}" for lat, lon in request.points)
|
||||
|
||||
# Эндпоинт для Table service
|
||||
url = f"{OSRM_BASE_URL}/table/v1/foot/{coords}?annotations=duration,distance"
|
||||
print(f"Запрос к OSRM: {url}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
resp = await client.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}")
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}")
|
||||
|
||||
data = resp.json()
|
||||
|
||||
distances = data.get("distances")
|
||||
durations = data.get("durations")
|
||||
|
||||
if distances is None or durations is None:
|
||||
raise HTTPException(status_code=500, detail="OSRM вернул некорректный ответ")
|
||||
|
||||
return TableResponse(distances=distances, durations=durations)
|
||||
|
||||
|
||||
@app.post("/route", response_model=RouteResponse)
|
||||
async def route(request: PointsRequest):
|
||||
"""
|
||||
Принимает две точки (начальную и конечную) и возвращает пошаговый маршрут.
|
||||
"""
|
||||
if len(request.points) != 2:
|
||||
raise HTTPException(status_code=400, detail="Для маршрута нужно ровно 2 точки")
|
||||
|
||||
coords = ";".join(f"{lon},{lat}" for lat, lon in request.points)
|
||||
|
||||
# Эндпоинт для Route service с параметром steps=true
|
||||
url = f"{OSRM_BASE_URL}/route/v1/foot/{coords}?steps=true"
|
||||
print(f"Запрос к OSRM: {url}")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
resp = await client.get(url, timeout=10)
|
||||
resp.raise_for_status()
|
||||
except httpx.RequestError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}")
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}")
|
||||
|
||||
data = resp.json()
|
||||
|
||||
if "routes" not in data or len(data["routes"]) == 0:
|
||||
raise HTTPException(status_code=500, detail="OSRM не вернул маршруты")
|
||||
|
||||
# Извлекаем шаги из ответа
|
||||
raw_steps = data["routes"][0]["legs"][0]["steps"]
|
||||
|
||||
formatted_steps = []
|
||||
for step in raw_steps:
|
||||
maneuver = step["maneuver"]
|
||||
formatted_steps.append(
|
||||
RouteStep(
|
||||
location=[maneuver["location"][1], maneuver["location"][0]],
|
||||
distance=step["distance"],
|
||||
type=maneuver["type"],
|
||||
modifier=maneuver.get("modifier"),
|
||||
name=step.get("name", "")
|
||||
)
|
||||
)
|
||||
|
||||
return RouteResponse(steps=formatted_steps)
|
||||
|
||||
13
engine-wrapper/pyproject.toml
Normal file
13
engine-wrapper/pyproject.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "engine-wrapper"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"dotenv>=0.9.9",
|
||||
"fastapi>=0.119.1",
|
||||
"httpx>=0.28.1",
|
||||
"pydantic>=2.12.3",
|
||||
"uvicorn>=0.38.0",
|
||||
]
|
||||
5
engine-wrapper/requirements.txt
Normal file
5
engine-wrapper/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
httpx
|
||||
pydantic
|
||||
python-dotenv
|
||||
275
engine-wrapper/uv.lock
generated
Normal file
275
engine-wrapper/uv.lock
generated
Normal file
@@ -0,0 +1,275 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
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, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "engine-wrapper"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastapi", specifier = ">=0.119.1" },
|
||||
{ name = "httpx", specifier = ">=0.28.1" },
|
||||
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.119.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/f4/152127681182e6413e7a89684c434e19e7414ed7ac0c632999c3c6980640/fastapi-0.119.1.tar.gz", hash = "sha256:a5e3426edce3fe221af4e1992c6d79011b247e3b03cc57999d697fe76cbf8ae0", size = 338616, upload-time = "2025-10-20T11:30:27.734Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/26/e6d959b4ac959fdb3e9c4154656fc160794db6af8e64673d52759456bf07/fastapi-0.119.1-py3-none-any.whl", hash = "sha256:0b8c2a2cce853216e150e9bd4faaed88227f8eb37de21cb200771f491586a27f", size = 108123, upload-time = "2025-10-20T11:30:26.185Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.48.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
||||
]
|
||||
3
input-to-route/.dockerignore
Normal file
3
input-to-route/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
.venv
|
||||
.env
|
||||
3
input-to-route/.gitignore
vendored
Normal file
3
input-to-route/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
.venv
|
||||
.env
|
||||
1
input-to-route/.python-version
Normal file
1
input-to-route/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.13
|
||||
13
input-to-route/Dockerfile
Normal file
13
input-to-route/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM python:3.13-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
0
input-to-route/README.md
Normal file
0
input-to-route/README.md
Normal file
BIN
input-to-route/addresses.sqlite
Normal file
BIN
input-to-route/addresses.sqlite
Normal file
Binary file not shown.
94
input-to-route/database.py
Normal file
94
input-to-route/database.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import json
|
||||
|
||||
|
||||
def search_database(database_file, query):
|
||||
"""
|
||||
Поиск по базе данных с группировкой тегов по приоритетам.
|
||||
|
||||
Args:
|
||||
database: список словарей с данными
|
||||
query: список списков тегов, например [["Памятник"], ["Архитектура"]]
|
||||
|
||||
Returns:
|
||||
список найденных записей с добавленными полями priority и измененным category_id
|
||||
"""
|
||||
with open(database_file, 'r', encoding='utf-8') as f:
|
||||
database = json.load(f)
|
||||
tag_mapping = {}
|
||||
tag_counter = 1
|
||||
|
||||
# Создаем маппинг тегов
|
||||
any_mode = False
|
||||
for priority_group_idx, tag_group in enumerate(query, start=1):
|
||||
if any_mode:
|
||||
tag_counter+=1
|
||||
if 'ANY' in tag_group:
|
||||
tag_group.remove('ANY')
|
||||
any_mode = True
|
||||
else:
|
||||
any_mode = False
|
||||
|
||||
if 'MULTI' in tag_group:
|
||||
tag_group.remove('MULTI')
|
||||
multi_mode = True
|
||||
else:
|
||||
multi_mode = False
|
||||
|
||||
for tag in tag_group:
|
||||
if tag not in tag_mapping:
|
||||
tag_mapping[tag] = {
|
||||
'priority': priority_group_idx,
|
||||
'tag_number': tag_counter,
|
||||
'multi': multi_mode
|
||||
}
|
||||
if not any_mode:
|
||||
tag_counter += 1
|
||||
|
||||
# Поиск и обработка записей
|
||||
results = []
|
||||
seen_ids = set()
|
||||
|
||||
for entry in database:
|
||||
categories = entry.get('category_id')
|
||||
categories =list(categories.split(', '))
|
||||
found = False
|
||||
for ctg in categories:
|
||||
if ctg in tag_mapping:
|
||||
found = True
|
||||
category= ctg
|
||||
break
|
||||
if found:
|
||||
entry_id = (entry.get('coordinate'), entry.get('title'))
|
||||
|
||||
if entry_id not in seen_ids:
|
||||
seen_ids.add(entry_id)
|
||||
result_entry = entry.copy()
|
||||
result_entry['type'] = tag_mapping[category]['tag_number']
|
||||
result_entry['priority'] = tag_mapping[category]['priority']
|
||||
results.append(result_entry)
|
||||
|
||||
# Сортируем по приоритету
|
||||
tag_priority = {v['tag_number']: (v['priority'],v['multi']) for v in tag_mapping.values()}
|
||||
# Then verify no conflicts
|
||||
if len(set((v['tag_number'], v['priority']) for v in tag_mapping.values())) != len(tag_priority):
|
||||
raise ValueError("Conflicting priorities for same tag_number")
|
||||
return results,tag_priority
|
||||
|
||||
|
||||
# Пример использования
|
||||
if __name__ == "__main__":
|
||||
# Загрузка базы данных из файла
|
||||
with open('output.json', 'r', encoding='utf-8') as f:
|
||||
database = json.load(f)
|
||||
|
||||
# Запрос с группами тегов
|
||||
query = [["Памятник", "Музей"], ["Архитектура"], ["Парк", "Сквер"]]
|
||||
|
||||
# Выполнение поиска
|
||||
results = search_database(database, query)
|
||||
print(results)
|
||||
# Сохранение результатов
|
||||
with open('search_results.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Найдено записей: {len(results)}")
|
||||
37
input-to-route/gemini_model.py
Normal file
37
input-to-route/gemini_model.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import openai
|
||||
|
||||
class GeminiModel:
|
||||
|
||||
def __init__(self):
|
||||
load_dotenv()
|
||||
self.openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
|
||||
self.openrouter_api_url = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1")
|
||||
|
||||
def call(self, prompt):
|
||||
if not self.openrouter_api_key:
|
||||
raise ValueError("OPENROUTER_API_KEY is not set in environment variables.")
|
||||
|
||||
client = openai.OpenAI(
|
||||
base_url=self.openrouter_api_url,
|
||||
api_key=self.openrouter_api_key,
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.chat.completions.create(
|
||||
model="google/gemini-2.5-flash",
|
||||
temperature=0.5,
|
||||
messages=[
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
)
|
||||
data = response.to_dict()
|
||||
return data['choices'][0]['message']['content']
|
||||
except openai.APIError as e:
|
||||
print(f"OpenRouter API call failed: {e}")
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
model = GeminiModel()
|
||||
print(model.call("Сколько будет 2 + 2?"))
|
||||
386
input-to-route/geocoder.py
Normal file
386
input-to-route/geocoder.py
Normal file
@@ -0,0 +1,386 @@
|
||||
import re
|
||||
import sqlite3
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
import json
|
||||
from rapidfuzz import process, fuzz, utils
|
||||
import math
|
||||
from libpostal_service import LibpostalService
|
||||
|
||||
lp_service = LibpostalService()
|
||||
# ---------------------------
|
||||
# Минимальная предобработка
|
||||
# ---------------------------
|
||||
|
||||
_DASHES = {"–": "-", "—": "-", "−": "-"}
|
||||
|
||||
def _preclean_text(s: str) -> str:
|
||||
"""Минимальная очистка для устойчивости парсера: регистр, дефисы, пробелы, буква-цифра разделение."""
|
||||
s = (s or "").strip().lower().replace("ё", "е")
|
||||
for k, v in _DASHES.items():
|
||||
s = s.replace(k, v)
|
||||
s = re.sub(r"\s+", " ", s)
|
||||
# Вставить пробел на границе буква<->цифра, чтобы "дом43" -> "дом 43"
|
||||
s = re.sub(r"(?<=[A-Za-zА-Яа-я])(?=\d)|(?<=\d)(?=[A-Za-zА-Яа-я])", " ", s)
|
||||
return s.strip()
|
||||
|
||||
def _parts_from_libpostal(addr_text: str) -> Dict[str, str]:
|
||||
"""Собирает метка->значение из parse_address без сложных правил, работая с ответом от сервиса."""
|
||||
parts: Dict[str, str] = {}
|
||||
|
||||
lp_response = lp_service.parse_address(addr_text)
|
||||
|
||||
for item in lp_response:
|
||||
label = item.get('label')
|
||||
comp = item.get('value')
|
||||
|
||||
if not comp or not label:
|
||||
continue
|
||||
|
||||
comp = " ".join(comp.split())
|
||||
if not comp:
|
||||
continue
|
||||
parts[label] = (parts[label] + " " + comp).strip() if label in parts else comp
|
||||
return parts
|
||||
|
||||
def _prefer_cyrillic_min(expansions: List[str]) -> Optional[str]:
|
||||
"""Выбрать «лучший» вариант из expand_address: предпочесть кириллицу и минимальную длину."""
|
||||
if not expansions:
|
||||
return None
|
||||
cyr = [e for e in expansions if re.search(r"[А-Яа-я]", e)]
|
||||
pool = cyr if cyr else expansions
|
||||
pool = [" ".join(e.split()) for e in pool]
|
||||
pool.sort(key=len)
|
||||
return pool[0] if pool else None
|
||||
|
||||
def canonicalize_road(road: str) -> str:
|
||||
"""Канонизация названия улицы через expand_address с минимальной эвристикой выбора."""
|
||||
if not road:
|
||||
return ""
|
||||
exp = lp_service.expand_address(road)
|
||||
return _prefer_cyrillic_min(exp) or road
|
||||
|
||||
def _clean_token(s: str) -> str:
|
||||
return " ".join((s or "").strip().lower().replace("ё", "е").split())
|
||||
|
||||
def _unit_suffix_and_value(text: str) -> str:
|
||||
"""
|
||||
Преобразует текст unit/доп.компонента в компактный русскоязычный суффикс:
|
||||
- 'корпус 1' / 'к 1' -> 'К1'
|
||||
- 'строение 2' / 'стр 2' -> 'С2'
|
||||
- 'лит а' -> 'ЛА'
|
||||
- 'влад 5' -> 'ВЛ5'
|
||||
Если тип не распознан, по умолчанию считаем корпус 'К' (без тяжёлых регексов).
|
||||
"""
|
||||
t = _clean_token(text)
|
||||
# выделяем буквы/цифры (минимум регексов: только удалить пробелы)
|
||||
val = t.replace(" ", "")
|
||||
# эвристики по подстрокам
|
||||
if "строен" in t or t.startswith("стр"):
|
||||
# строение
|
||||
val = val.replace("строение", "").replace("стр", "")
|
||||
return f"С{val}".upper() if val else "С"
|
||||
if "корп" in t or t == "к" or t.startswith("к"):
|
||||
# корпус
|
||||
val = val.replace("корпус", "").replace("корп", "").replace("к", "")
|
||||
return f"К{val}".upper() if val else "К"
|
||||
if t.startswith("лит"):
|
||||
# литера
|
||||
val = val.replace("литера", "").replace("лит", "")
|
||||
return f"Л{val}".upper() if val else "Л"
|
||||
if t.startswith("влад"):
|
||||
# владение
|
||||
val = val.replace("владение", "").replace("влад", "")
|
||||
return f"ВЛ{val}".upper() if val else "ВЛ"
|
||||
# вход/подъезд/лестница можно при желании кодировать как 'П', 'ЛС', но обычно это не часть addr:housenumber в OSM
|
||||
# по умолчанию — трактуем как корпус
|
||||
# оставим только цифро-буквенную часть после удаления пробелов
|
||||
return f"К{val}".upper() if val else ""
|
||||
|
||||
def build_house_number_from_parts(parts: Dict[str, str], original_line: str) -> str:
|
||||
"""
|
||||
Формирует финальный housenumber из libpostal:
|
||||
- base = house_number
|
||||
- добавляет суффиксы из unit/entrance/staircase/level, конвертируя их в 'К..'/'С..'/'Л..'/'ВЛ..'
|
||||
"""
|
||||
base = (parts.get("house_number") or "").strip()
|
||||
# защитимся от «дом43» и т.п. минимальной разделиловкой (сделано ранее в _preclean_text)
|
||||
suffixes = []
|
||||
|
||||
# unit часто содержит корпус/строение
|
||||
if parts.get("unit"):
|
||||
suffixes.append(_unit_suffix_and_value(parts["unit"]))
|
||||
|
||||
# некоторые адреса могут класть строение/литеру в entrance/staircase
|
||||
for key in ("entrance", "staircase"):
|
||||
if parts.get(key):
|
||||
sfx = _unit_suffix_and_value(parts[key])
|
||||
if sfx:
|
||||
suffixes.append(sfx)
|
||||
|
||||
# level — это этаж, обычно не часть housenumber в OSM; по умолчанию не добавляем
|
||||
|
||||
# если базовый номер пуст — попробуем через expand_address всей строки (без сложных правил)
|
||||
if not base:
|
||||
for e in lp_service.expand_address(original_line):
|
||||
p2 = _parts_from_libpostal(e)
|
||||
b2 = (p2.get("house_number") or "").strip()
|
||||
if b2:
|
||||
base = b2
|
||||
if p2.get("unit") and not suffixes:
|
||||
suffixes.append(_unit_suffix_and_value(p2["unit"]))
|
||||
break
|
||||
|
||||
# склеиваем: пробелы убираем, верхний регистр для стабильности
|
||||
hn = "".join(base.split()).upper() + "".join(suffixes)
|
||||
return hn
|
||||
|
||||
def normalize_street_for_index(street_original: str) -> str:
|
||||
"""Нормализация ключа улицы для индекса (регистронезависимая, с лексической очисткой)."""
|
||||
# Используем обработчик RapidFuzz по умолчанию + канонизацию libpostal
|
||||
street_clean = _preclean_text(street_original)
|
||||
street_canon = canonicalize_road(street_clean)
|
||||
return utils.default_process(street_canon) or street_canon
|
||||
|
||||
def normalize_house_for_index(house_original: str) -> str:
|
||||
"""Простая нормализация номера для индекса: убрать пробелы, привести к верхнему регистру."""
|
||||
return "".join((house_original or "").split()).upper()
|
||||
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# Поиск/валидация адреса
|
||||
# ---------------------------
|
||||
|
||||
def parse_with_libpostal(addr: str) -> Dict[str, str]:
|
||||
"""
|
||||
Минимально-шумоустойчивая раскладка через libpostal.
|
||||
"""
|
||||
txt = _preclean_text(addr)
|
||||
parts = _parts_from_libpostal(txt)
|
||||
print("Parts:", parts)
|
||||
road = parts.get("road") or parts.get("pedestrian") or parts.get("footway") or parts.get("residential") or ""
|
||||
house = build_house_number_from_parts(parts, txt)
|
||||
city = parts.get("city") or parts.get("city_district") or parts.get("suburb") or ""
|
||||
postcode = parts.get("postcode", "")
|
||||
# Канонизируем улицу для сравнения
|
||||
road_canon = canonicalize_road(road)
|
||||
return {
|
||||
"road": road_canon or road or "",
|
||||
"house_number": house or "",
|
||||
"city": city or "",
|
||||
"postcode": postcode or ""
|
||||
}
|
||||
|
||||
def best_street_match(conn: sqlite3.Connection, street_query: str, cutoff: int = 86) -> Optional[Tuple[str, str]]:
|
||||
"""
|
||||
Ищет с учётом опечаток: exact по нормализованному ключу, иначе WRatio к ближайшему.
|
||||
"""
|
||||
s_key = normalize_street_for_index(street_query)
|
||||
cur = conn.execute("SELECT street_original FROM street_names WHERE street_norm = ?", (s_key,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return (s_key, row[0])
|
||||
|
||||
cur = conn.execute("SELECT street_norm, street_original FROM street_names")
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return None
|
||||
choices = [r[0] for r in rows]
|
||||
match = process.extractOne(s_key, choices, scorer=fuzz.WRatio, processor=None, score_cutoff=cutoff)
|
||||
if match:
|
||||
matched_norm = match[0]
|
||||
for s_n, s_o in rows:
|
||||
if s_n == matched_norm:
|
||||
return (s_n, s_o)
|
||||
return None
|
||||
|
||||
def _base_digits(s: str) -> str:
|
||||
m = re.search(r"\d+", s or "")
|
||||
return m.group(0) if m else ""
|
||||
|
||||
def best_house_match(conn, street_norm: str, house_query: str, cutoff: int = 82) -> Optional[Tuple[str, str]]:
|
||||
# Простой канон запроса: убрать пробелы, верхний регистр
|
||||
q_norm = ("".join((house_query or "").split())).upper()
|
||||
q_base = _base_digits(q_norm)
|
||||
|
||||
# Вытаскиваем все номера по улице
|
||||
cur = conn.execute(
|
||||
"SELECT housenumber_norm, housenumber_original FROM address_index WHERE street_norm = ?",
|
||||
(street_norm,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
# 1) Точное совпадение целиком
|
||||
for hn, ho in rows:
|
||||
if hn == q_norm:
|
||||
return (hn, ho)
|
||||
|
||||
# 2) Жёсткий фильтр по базовому числу (обязательное совпадение)
|
||||
if q_base:
|
||||
filtered: List[Tuple[str, str]] = [(hn, ho) for hn, ho in rows if _base_digits(hn) == q_base]
|
||||
else:
|
||||
filtered = rows
|
||||
|
||||
if not filtered:
|
||||
return None
|
||||
|
||||
# 3) Нечёткий матч только среди отфильтрованных кандидатов
|
||||
choices = [hn for hn, _ in filtered]
|
||||
match = process.extractOne(q_norm, choices, scorer=fuzz.WRatio, processor=None, score_cutoff=cutoff)
|
||||
if match:
|
||||
best_hn = match[0]
|
||||
for hn, ho in filtered:
|
||||
if hn == best_hn:
|
||||
return (hn, ho)
|
||||
|
||||
# 4) Fallback: если остался один кандидат с таким же базовым числом — принять его
|
||||
if len(filtered) == 1:
|
||||
return filtered[0]
|
||||
|
||||
return None
|
||||
|
||||
def _interp_point_along(coords: List[List[float]], t: float) -> Dict[str,float]:
|
||||
# coords: [[lon,lat], ...], t∈[0,1] по длине полилинии
|
||||
def dist(a,b):
|
||||
dx=a[0]-b[0]; dy=a[1]-b[1]; return math.hypot(dx, dy)
|
||||
seg_len=[]; total=0.0
|
||||
for i in range(len(coords)-1):
|
||||
d=dist(coords[i], coords[i+1]); seg_len.append(d); total += d
|
||||
if total==0.0:
|
||||
return {"lat": coords[0][1], "lon": coords[0][0]}
|
||||
target = t*total; acc=0.0
|
||||
for i,d in enumerate(seg_len):
|
||||
if acc + d >= target:
|
||||
ratio = 0.0 if d==0 else (target-acc)/d
|
||||
lon = coords[i][0] + (coords[i+1][0]-coords[i][0])*ratio
|
||||
lat = coords[i][1] + (coords[i+1][1]-coords[i][1])*ratio
|
||||
return {"lat": float(lat), "lon": float(lon)}
|
||||
acc += d
|
||||
return {"lat": coords[-1][1], "lon": coords[-1][0]}
|
||||
|
||||
def _fits_type(n:int, itype:str)->bool:
|
||||
itype=(itype or "").lower()
|
||||
if itype=="odd": return n%2==1
|
||||
if itype=="even": return n%2==0
|
||||
return True # 'all' или неизвестный тип
|
||||
|
||||
def find_interpolation_coord(conn: sqlite3.Connection, street_norm: str, house_query: str) -> Optional[Dict[str,float]]:
|
||||
m = re.search(r"\d+", house_query or "")
|
||||
if not m:
|
||||
return None
|
||||
n = int(m.group(0))
|
||||
cur = conn.execute(
|
||||
"SELECT itype, start_num, end_num, coords_json FROM interpolation_ways WHERE street_norm=?",
|
||||
(street_norm,)
|
||||
)
|
||||
best=None
|
||||
for itype, s_i, e_i, coords_json in cur.fetchall():
|
||||
if n < min(s_i,e_i) or n > max(s_i,e_i):
|
||||
continue
|
||||
if not _fits_type(n, itype):
|
||||
continue
|
||||
coords = json.loads(coords_json)
|
||||
# позиция вдоль диапазона с плавающей точкой
|
||||
span = (e_i - s_i) if (e_i - s_i)!=0 else 1
|
||||
t = (n - s_i) / span
|
||||
t = max(0.0, min(1.0, t))
|
||||
return _interp_point_along(coords, t)
|
||||
return best
|
||||
|
||||
def get_street_centroid_any(conn: sqlite3.Connection, street_norm: str) -> Optional[Dict[str, float]]:
|
||||
# 1) по адресным точкам
|
||||
cur = conn.execute(
|
||||
"SELECT AVG(lat), AVG(lon) FROM address_index WHERE street_norm=? AND lat IS NOT NULL AND lon IS NOT NULL",
|
||||
(street_norm,)
|
||||
)
|
||||
lat, lon = cur.fetchone() or (None,None)
|
||||
if lat is not None and lon is not None:
|
||||
return {"lat": float(lat), "lon": float(lon)}
|
||||
# 2) по геометрии улиц (highway)
|
||||
cur = conn.execute("SELECT lat, lon FROM street_center WHERE street_norm=?",(street_norm,))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {"lat": float(row[0]), "lon": float(row[1])}
|
||||
return None
|
||||
|
||||
def validate_address(sqlite_path: str, address_str: str) -> Dict[str, Any]:
|
||||
comps = parse_with_libpostal(address_str)
|
||||
street_in = comps.get("road", "")
|
||||
house_in = comps.get("house_number", "")
|
||||
|
||||
conn = sqlite3.connect(sqlite_path)
|
||||
try:
|
||||
# улица обязательна
|
||||
if not street_in:
|
||||
return {
|
||||
"valid": False,
|
||||
"corrected": None,
|
||||
"components": comps,
|
||||
"coordinates": None,
|
||||
"reason": "Не удалось распознать улицу"
|
||||
}
|
||||
|
||||
st = best_street_match(conn, street_in)
|
||||
if not st:
|
||||
return {
|
||||
"valid": False,
|
||||
"corrected": None,
|
||||
"components": comps,
|
||||
"coordinates": None,
|
||||
"reason": "Улица не найдена в локальной базе"
|
||||
}
|
||||
street_norm, street_orig = st
|
||||
|
||||
# 1) дом не введён: вернуть координату улицы (centroid)
|
||||
if not house_in:
|
||||
cent = get_street_centroid_any(conn, street_norm)
|
||||
return {
|
||||
"valid": True if cent else False,
|
||||
"corrected": street_orig,
|
||||
"components": comps,
|
||||
"coordinates": cent,
|
||||
"reason": None if cent else "Для улицы нет координатной опоры"
|
||||
}
|
||||
|
||||
# 2) дом введён: пробуем точное сопоставление
|
||||
hn = best_house_match(conn, street_norm, house_in)
|
||||
if hn:
|
||||
house_norm, house_orig = hn
|
||||
cur = conn.execute(
|
||||
"SELECT city, postcode, lat, lon FROM address_index WHERE street_norm=? AND housenumber_norm=? LIMIT 1",
|
||||
(street_norm, house_norm)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
city, postcode, lat, lon = (row or (None,None,None,None))
|
||||
coords = {"lat": float(lat), "lon": float(lon)} if lat is not None and lon is not None else None
|
||||
# если почему-то lat/lon пустые (редкий случай), дополнительно попробуем интерполяцию/центроид
|
||||
if coords is None:
|
||||
coords = find_interpolation_coord(conn, street_norm, house_norm) or get_street_centroid_any(conn, street_norm)
|
||||
corrected = ", ".join(x for x in [f"{street_orig} {house_orig}", city, postcode] if x and str(x).strip())
|
||||
return {
|
||||
"valid": True,
|
||||
"corrected": corrected,
|
||||
"components": comps,
|
||||
"coordinates": coords,
|
||||
"reason": None
|
||||
}
|
||||
|
||||
# 3) дом введён, но не найден: invalid, однако координаты улицы — обязательно
|
||||
fallback = find_interpolation_coord(conn, street_norm, house_in) or get_street_centroid_any(conn, street_norm)
|
||||
return {
|
||||
"valid": False,
|
||||
"corrected": None,
|
||||
"components": comps,
|
||||
"coordinates": fallback,
|
||||
"reason": "Номер дома отсутствует на найденной улице"
|
||||
}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
res = validate_address("addresses.sqlite", "Дальняя 8")
|
||||
print(res)
|
||||
pass
|
||||
38
input-to-route/libpostal_service.py
Normal file
38
input-to-route/libpostal_service.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
import requests
|
||||
|
||||
class LibpostalService:
|
||||
|
||||
def __init__(self):
|
||||
load_dotenv()
|
||||
self.libpostal_url = os.getenv("LIBPOSTAL_URL")
|
||||
|
||||
def parse_address(self, address_string):
|
||||
if not self.libpostal_url:
|
||||
raise ValueError("LIBPOSTAL_URL is not set in environment variables.")
|
||||
|
||||
url = f"{self.libpostal_url}/parse"
|
||||
print(url)
|
||||
params = {"address": address_string}
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error parsing address: {e}")
|
||||
return None
|
||||
|
||||
def expand_address(self, address_string):
|
||||
if not self.libpostal_url:
|
||||
raise ValueError("LIBPOSTAL_URL is not set in environment variables.")
|
||||
|
||||
url = f"{self.libpostal_url}/expand"
|
||||
params = {"address": address_string}
|
||||
try:
|
||||
response = requests.get(url, params=params)
|
||||
response.raise_for_status() # Raise an exception for HTTP errors
|
||||
return response.json()
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error expanding address: {e}")
|
||||
return None
|
||||
143
input-to-route/main.py
Normal file
143
input-to-route/main.py
Normal file
@@ -0,0 +1,143 @@
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from new_parser_test import parse_request
|
||||
from geocoder import validate_address
|
||||
from database import search_database
|
||||
from route import build_route
|
||||
from ouput_chat import RouteDescriber
|
||||
|
||||
app = FastAPI(title="AI API")
|
||||
|
||||
# ======== FASTAPI СХЕМЫ ==========
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
user_input: str
|
||||
conversation_history: list | None = None
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
success: bool
|
||||
need_more_info: bool
|
||||
# result теперь может содержать как промежуточные данные парсера,
|
||||
# так и финальный маршрут с описанием
|
||||
result: dict
|
||||
conversation_history: list
|
||||
error: str | None = None
|
||||
|
||||
# =================================
|
||||
|
||||
@app.post("/chat", response_model=ChatResponse)
|
||||
def chat_endpoint(request: ChatRequest):
|
||||
try:
|
||||
# 1. Вызываем парсер, чтобы извлечь информацию из диалога
|
||||
parser_response = parse_request(request.user_input, request.conversation_history)
|
||||
|
||||
# 2. Проверяем условие: парсер отработал успешно и больше информации не нужно
|
||||
if parser_response["success"] and not parser_response["need_more_info"]:
|
||||
# --- Логика построения маршрута ---
|
||||
|
||||
parsed_data = parser_response["result"]
|
||||
tags = parsed_data.get("tags")
|
||||
print(tags)
|
||||
user_address = parsed_data.get("user_location")
|
||||
print(user_address)
|
||||
user_time = parsed_data.get("time")
|
||||
print(user_time)
|
||||
conversation_history = parser_response["conversation_history"]
|
||||
|
||||
# Валидация адреса
|
||||
val_output = validate_address('addresses.sqlite', user_address)
|
||||
if not val_output or not val_output['valid']:
|
||||
return ChatResponse(
|
||||
success=False,
|
||||
need_more_info=False,
|
||||
result={},
|
||||
conversation_history=conversation_history,
|
||||
error=f"Не удалось распознать адрес: {user_address}. Пожалуйста, попробуйте указать его точнее."
|
||||
)
|
||||
|
||||
user_position = [
|
||||
val_output['coordinates']['lat'],
|
||||
val_output['coordinates']['lon']
|
||||
]
|
||||
|
||||
# Поиск точек в базе данных
|
||||
found_points, mapping = search_database('output.json', tags)
|
||||
if not found_points:
|
||||
return ChatResponse(
|
||||
success=False,
|
||||
need_more_info=False,
|
||||
result={},
|
||||
conversation_history=conversation_history,
|
||||
error="К сожалению, я не смог найти подходящих мест по вашему запросу."
|
||||
)
|
||||
|
||||
print(mapping)
|
||||
|
||||
# Построение маршрута
|
||||
n_nodes = len(mapping)
|
||||
allow_extend = any(v[1] for v in mapping.values())
|
||||
|
||||
route_otp = None
|
||||
if allow_extend:
|
||||
while n_nodes <= 5 and (route_otp := build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random') or route_otp): n_nodes += 1
|
||||
else:
|
||||
route_otp= build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random')
|
||||
|
||||
if not route_otp:
|
||||
return ChatResponse(
|
||||
success=False,
|
||||
need_more_info=False,
|
||||
result={},
|
||||
conversation_history=conversation_history,
|
||||
error="Не удалось построить маршрут с указанными параметрами. Попробуйте изменить запрос."
|
||||
)
|
||||
|
||||
route, places = route_otp
|
||||
route.insert(0, user_position)
|
||||
|
||||
# Генерация описания маршрута
|
||||
describer = RouteDescriber()
|
||||
description = describer.generate_route_description(places, conversation_history)
|
||||
|
||||
# Финальный успешный ответ с маршрутом
|
||||
return ChatResponse(
|
||||
success=True,
|
||||
need_more_info=False,
|
||||
result={
|
||||
"route": route,
|
||||
"description": description,
|
||||
"places": places
|
||||
},
|
||||
conversation_history=conversation_history,
|
||||
error=None
|
||||
)
|
||||
|
||||
else:
|
||||
# 3. Если парсеру нужно больше информации или он завершился неуспешно,
|
||||
# просто возвращаем его ответ пользователю.
|
||||
# `ChatResponse` и словарь от `parse_request` имеют одинаковую структуру.
|
||||
return ChatResponse(**parser_response)
|
||||
|
||||
except Exception as e:
|
||||
# Отлавливаем любые другие непредвиденные ошибки
|
||||
# В рабочей среде здесь лучше добавить логирование
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
raise HTTPException(status_code=500, detail="Произошла внутренняя ошибка сервера.")
|
||||
|
||||
|
||||
origins = [
|
||||
"http://localhost",
|
||||
"http://localhost:5173",
|
||||
"https://gorkycode.nikidze.ru"
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
max_age=3600
|
||||
)
|
||||
381
input-to-route/new_parser_test.py
Normal file
381
input-to-route/new_parser_test.py
Normal file
@@ -0,0 +1,381 @@
|
||||
import json
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from gemini_model import GeminiModel
|
||||
|
||||
# Базовый список доступных тегов
|
||||
TAG_LIST = [
|
||||
"Памятник", "Архитектура", "Мозаика", "Панно",
|
||||
"Парк", "Водоем", "Озеро", "Ботанический сад",
|
||||
"Архитектура", "Площадь", "Набережная", "Канатная дорога",
|
||||
"Сквер", "Музей", "Собор", "Церковь",
|
||||
"Храм", "Театр", "Выставка", "Мост", "Художественное пространство", "Вокзал", "Фонтан", "Лестница", "Кинотеатр",
|
||||
"Дом Культуры", "Планетарий", "Галерея", "Ресторан", "Бар", "Бистро", "Кафе", "Кофейня", "Библиотека"
|
||||
]
|
||||
|
||||
|
||||
def build_extraction_prompt(conversation_history: List[Dict[str, str]], tag_list: List[str]) -> str:
|
||||
"""
|
||||
Строим промпт для ИИ-парсера с историей диалога
|
||||
"""
|
||||
example_user_1 = "Я хочу культурно провести время, у меня 2 часа, я сейчас на улице Доброй около дома 1."
|
||||
example_json_1 = {
|
||||
"tags": [["Музей", "Галерея", "Театр","Выставка","ANY", 'MULTI']],
|
||||
"user_location": "улица Добрая, дом 1",
|
||||
"time": 120
|
||||
}
|
||||
example_user_2 = "Я хочу сходить в музей пройти по парку и где-нибудь поесть, у меня 3.5 часа, я на Коминтерна, 115"
|
||||
example_json_2 = {
|
||||
"tags": [["Музей", "Парк"], ["Ресторан", "Бистро", "Кафе", "ANY"]],
|
||||
"user_location": "улица Коминтерна, дом 115",
|
||||
"time": 210
|
||||
}
|
||||
example_user_3 = "Я хочу культурно провести время потом попить кофе, я свободен до 6 вечера, я сейчас на б-р 60 лет Октября, 9"
|
||||
example_json_3 = {
|
||||
"tags": [['Музей', 'Галерея', 'Театр', 'Выставка', 'Художественное пространство', 'Планетарий', 'ANY', 'MULTI'], ["Кофейня"]],
|
||||
"user_location": "бульвар 60 лет Октября, дом 9",
|
||||
"time": 'until 18:00'
|
||||
}
|
||||
|
||||
example_user_4 = "Я хочу обойти места в которых можно попить кофе и зайти в музей, у меня 4 часа, я на дальней 8"
|
||||
example_json_4 = {
|
||||
"tags": [['Кофейня', 'MULTI'],
|
||||
["Музей"]],
|
||||
"user_location": "улица Дальняя, дом 8",
|
||||
"time": 240
|
||||
}
|
||||
|
||||
instr = f"""
|
||||
Ты — ИИ-парсер на русском языке.
|
||||
Задача: из диалога с пользователем извлечь строго JSON c полями:
|
||||
|
||||
- tags: упорядоченный список релевантных тегов ТОЛЬКО из данного tag_list (не придумывай новых), теги должны быть сгруппированы по приоритету посещения
|
||||
- user_location: строка с текущим местоположением пользователя (примерный адрес), должен содержать название улицы/площади/... Но не может быть ориентиром на объект без конкретного адреса или чем-то абстрактным: дома, у памятника Пушкину, у станции метро, на автобусной остановке... не подходят, но я около площади Революции подходит.
|
||||
- time: целое число минут на прогулку (преобразуй выражения типа "2 часа" -> 120, "полчаса" -> 30 и т.п.). Пользователь может указать до скольки он свободен тогда используй фиксированный формат until <время в 24 формате HH:MM>. 'Я хочу погулять до 6 вечера' -> тогда в поле время 'until 18:00'.
|
||||
|
||||
Работа с тегами:
|
||||
Группируй теги по ПРИОРИТЕТУ посещения:
|
||||
- Если пользователь говорит "сначала музей, потом кафе" -> [['Музей'], ['Кафе']]
|
||||
- Если "хочу в музей и кафе" (без явного порядка) -> [['Музей', 'Кафе']]
|
||||
- Если неопределённые предпочтения "погулять по интересным местам", добавь ключевое слово 'ANY' к группе подходящих тегов.
|
||||
- Если пользователь хочет посетить несколько мест одного типа, добавь ключевое слово 'MULTI' в соответсвующую группу. "Хочу походить по музеям, потом попить кофе." -> [['Музей','MULTI'], ['Кофейня']]
|
||||
- Запрос пользователя может быть абстрактным "я хочу погулять по улице". Подбери релевантные теги на открытом воздухе (т.к. погулять можно интерпретировать как походить по улице не заходя куда-либо) например: [['Архитектура','Сквер','Площадь', 'ANY']]. Запрос может быть абстрактным, но не может быть полностью неопределенным. Примеры неопределенных ответов: "Хочу куда-нибудь сходить", "Я не знаю куда сходить", "Куда мне сходить?"
|
||||
- Указывай все теги из tag_list которые явно подходят!!! Примеры могут быть неполными!
|
||||
|
||||
ИСПОЛЬЗУЙ ТОЛЬКО ТЕГИ ИЗ ДАННОГО СПИСКА!
|
||||
|
||||
Требования:
|
||||
- Верни ТОЛЬКО валидный JSON без префиксов, комментариев и форматирования в кодовых блоках.
|
||||
- Если в диалоге с пользователем нет данных для какого-либо поля, поставь null в этом поле.
|
||||
- Не добавляй никаких дополнительных полей, только: tags, user_location, time.
|
||||
- tags должны быть подмножеством из tag_list. Не включай нерелевантные теги.
|
||||
|
||||
tag_list = {json.dumps(tag_list, ensure_ascii=False, indent=0)}
|
||||
|
||||
Пример 1:
|
||||
Пользователь: "{example_user_1}"
|
||||
Ожидаемый JSON:
|
||||
{json.dumps(example_json_1, ensure_ascii=False, indent=2)}
|
||||
|
||||
Пример 2:
|
||||
Пользователь: "{example_user_2}"
|
||||
Ожидаемый JSON:
|
||||
{json.dumps(example_json_2, ensure_ascii=False, indent=2)}
|
||||
|
||||
Пример 3:
|
||||
Пользователь: "{example_user_3}"
|
||||
Ожидаемый JSON:
|
||||
{json.dumps(example_json_3, ensure_ascii=False, indent=2)}
|
||||
|
||||
Пример 4:
|
||||
Пользователь: "{example_user_4}"
|
||||
Ожидаемый JSON:
|
||||
{json.dumps(example_json_4, ensure_ascii=False, indent=2)}
|
||||
|
||||
История диалога:
|
||||
"""
|
||||
|
||||
for msg in conversation_history:
|
||||
role = msg["role"]
|
||||
content = msg["content"]
|
||||
if role == "user":
|
||||
instr += f"\nПользователь: \"{content}\""
|
||||
elif role == "assistant":
|
||||
instr += f"\nАссистент: \"{content}\""
|
||||
|
||||
instr += "\n\nТеперь извлеки JSON из диалога выше."
|
||||
|
||||
return instr.strip()
|
||||
|
||||
|
||||
def build_conversational_prompt(conversation_history: List[Dict[str, str]], missing: List[str],
|
||||
tag_list: List[str]) -> str:
|
||||
"""
|
||||
Строим промпт для генерации уточняющего вопроса
|
||||
"""
|
||||
missing_desc = {
|
||||
"tags": "какие места/категории интересны",
|
||||
"user_location": "текущее местоположение (адрес)",
|
||||
"time": "время на прогулку"
|
||||
}
|
||||
|
||||
missing_fields_str = ", ".join([missing_desc[f] for f in missing])
|
||||
|
||||
instr = f"""
|
||||
Ты — вежливый ИИ-ассистент, который помогает пользователю спланировать прогулку.
|
||||
Твоя задача — задать краткие вопросы, чтобы уточнить недостающую информацию.
|
||||
|
||||
Недостающие данные: {missing_fields_str}
|
||||
|
||||
Правила:
|
||||
- Задай ТОЛЬКО простык, вежливый и краткий вопрос.
|
||||
- НЕ давай рекомендаций, комментариев или предложений.
|
||||
- НЕ предлагай варианты, кроме случаев когда нужно выбрать категории (tags).
|
||||
- Если нужно уточнить категории (tags), предложи категории ИЗ ЭТОГО СПИСКА: {json.dumps(tag_list, ensure_ascii=False)}
|
||||
- Верни ТОЛЬКО текст вопроса без дополнительных пояснений.
|
||||
|
||||
История диалога:
|
||||
"""
|
||||
|
||||
for msg in conversation_history:
|
||||
role = msg["role"]
|
||||
content = msg["content"]
|
||||
if role == "user":
|
||||
instr += f"\nПользователь: {content}"
|
||||
elif role == "assistant":
|
||||
instr += f"\nАссистент: {content}"
|
||||
|
||||
instr += "\n\nТеперь задай уточняющий вопрос:"
|
||||
|
||||
return instr.strip()
|
||||
|
||||
|
||||
def call_gemini(prompt: str) -> str:
|
||||
return GeminiModel().call(prompt)
|
||||
|
||||
|
||||
def strip_code_fences(text: str) -> str:
|
||||
s = text.strip()
|
||||
# Убираем ограду `````` или ``````
|
||||
if s.startswith("```"):
|
||||
# срезаем первые ```
|
||||
s = s[3:].lstrip()
|
||||
# если указан язык (например, json), уберём первую строку
|
||||
first_nl = s.find("\n")
|
||||
if first_nl != -1:
|
||||
lang = s[:first_nl].strip().lower()
|
||||
# если это похоже на метку языка, отбрасываем её
|
||||
if lang in ("json", "yaml", "yml", "xml", "markdown", "md", "txt"):
|
||||
s = s[first_nl + 1:]
|
||||
else:
|
||||
# если метка не признана, всё равно продолжаем с текущим s
|
||||
pass
|
||||
# убираем завершающие ```
|
||||
if s.rstrip().endswith("```"):
|
||||
s = s.rstrip()[:-3]
|
||||
|
||||
return s.strip()
|
||||
|
||||
|
||||
def extract_first_json(text: str):
|
||||
# 1) снимаем ограду
|
||||
s = strip_code_fences(text)
|
||||
|
||||
# 2) пробуем распарсить как есть
|
||||
try:
|
||||
return json.loads(s)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3) ищем первый сбалансированный объект { ... } без рекурсивного regex
|
||||
start = s.find("{")
|
||||
while start != -1:
|
||||
depth = 0
|
||||
in_str = False
|
||||
esc = False
|
||||
for i in range(start, len(s)):
|
||||
ch = s[i]
|
||||
if in_str:
|
||||
if esc:
|
||||
esc = False
|
||||
elif ch == "\\":
|
||||
esc = True
|
||||
elif ch == '"':
|
||||
in_str = False
|
||||
else:
|
||||
if ch == '"':
|
||||
in_str = True
|
||||
elif ch == "{":
|
||||
depth += 1
|
||||
elif ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
cand = s[start:i + 1]
|
||||
try:
|
||||
return json.loads(cand)
|
||||
except Exception:
|
||||
break
|
||||
# ищем следующий '{', если текущий блок не распарсился
|
||||
start = s.find("{", start + 1)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_complete(result: Dict[str, Any]) -> bool:
|
||||
if not isinstance(result, dict):
|
||||
return False
|
||||
|
||||
# Проверяем наличие ключей
|
||||
for key in ["tags", "user_location", "time"]:
|
||||
if key not in result:
|
||||
return False
|
||||
|
||||
# Проверяем содержимое
|
||||
tags_ok = isinstance(result["tags"], list) and len(result["tags"]) > 0
|
||||
loc_ok = isinstance(result["user_location"], str) and len(result["user_location"].strip()) > 0
|
||||
|
||||
# time может быть int или строка формата "until HH:MM"
|
||||
time_val = result["time"]
|
||||
if isinstance(time_val, int) and time_val > 0:
|
||||
time_ok = True
|
||||
elif isinstance(time_val, str) and time_val.startswith("until "):
|
||||
time_ok = True
|
||||
else:
|
||||
time_ok = False
|
||||
|
||||
return tags_ok and loc_ok and time_ok
|
||||
|
||||
|
||||
def missing_fields(result: Dict[str, Any]) -> List[str]:
|
||||
missing = []
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return ["tags", "user_location", "time"]
|
||||
|
||||
if "tags" not in result or not isinstance(result["tags"], list) or len(result["tags"]) == 0:
|
||||
missing.append("tags")
|
||||
|
||||
if "user_location" not in result or not isinstance(result["user_location"], str) or not result[
|
||||
"user_location"].strip():
|
||||
missing.append("user_location")
|
||||
|
||||
time_val = result.get("time")
|
||||
if isinstance(time_val, int) and time_val > 0:
|
||||
pass # OK
|
||||
elif isinstance(time_val, str) and time_val.startswith("until "):
|
||||
pass # OK
|
||||
else:
|
||||
missing.append("time")
|
||||
|
||||
return missing
|
||||
|
||||
|
||||
def convert_time_to_minutes(time_val, current_time: datetime) -> int:
|
||||
"""
|
||||
Конвертирует значение времени в минуты.
|
||||
Если time_val — это строка формата "until HH:MM", вычисляет разницу от текущего времени.
|
||||
"""
|
||||
if isinstance(time_val, int):
|
||||
return time_val
|
||||
|
||||
if isinstance(time_val, str) and time_val.startswith("until "):
|
||||
time_str = time_val.replace("until ", "").strip()
|
||||
try:
|
||||
target_time = datetime.strptime(time_str, "%H:%M")
|
||||
# Устанавливаем дату как текущую
|
||||
target_datetime = current_time.replace(
|
||||
hour=target_time.hour,
|
||||
minute=target_time.minute,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
# Если целевое время уже прошло сегодня, предполагаем завтра
|
||||
if target_datetime <= current_time:
|
||||
target_datetime += timedelta(days=1)
|
||||
|
||||
delta = target_datetime - current_time
|
||||
minutes = int(delta.total_seconds() / 60)
|
||||
return minutes
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def extract_with_ai(conversation_history: List[Dict[str, str]]) -> Dict[str, Any]:
|
||||
prompt = build_extraction_prompt(conversation_history, TAG_LIST)
|
||||
model_text = call_gemini(prompt)
|
||||
parsed = extract_first_json(model_text) or {}
|
||||
return parsed
|
||||
|
||||
|
||||
def ask_ai_for_clarification(conversation_history: List[Dict[str, str]], missing: List[str]) -> str:
|
||||
"""
|
||||
Генерируем уточняющий вопрос через ИИ
|
||||
"""
|
||||
prompt = build_conversational_prompt(conversation_history, missing, TAG_LIST)
|
||||
question = call_gemini(prompt)
|
||||
return question.strip()
|
||||
|
||||
|
||||
def parse_request(user_input=None, conversation_history=None):
|
||||
if conversation_history is None:
|
||||
conversation_history = []
|
||||
|
||||
if len(conversation_history) > 8:
|
||||
return {
|
||||
"success": False,
|
||||
"need_more_info": False,
|
||||
"result": {},
|
||||
"conversation_history": conversation_history,
|
||||
"error": "Слишком длинная история диалога. Начните заново."
|
||||
}
|
||||
|
||||
if user_input:
|
||||
conversation_history.append({"role": "user", "content": user_input})
|
||||
|
||||
current_time = datetime.now()
|
||||
result = extract_with_ai(conversation_history)
|
||||
|
||||
max_tries = 3
|
||||
tries = 0
|
||||
need_more_info = False
|
||||
|
||||
while not is_complete(result) and tries < max_tries:
|
||||
missing = missing_fields(result)
|
||||
if not missing:
|
||||
break
|
||||
|
||||
ai_question = ask_ai_for_clarification(conversation_history, missing)
|
||||
conversation_history.append({"role": "assistant", "content": ai_question})
|
||||
need_more_info = True
|
||||
break # API не ждёт ввода, просто возвращает вопрос пользователю
|
||||
|
||||
if "time" in result and isinstance(result["time"], str):
|
||||
minutes = convert_time_to_minutes(result["time"], current_time)
|
||||
if minutes is not None:
|
||||
result["time"] = minutes
|
||||
|
||||
out = {
|
||||
"tags": result.get("tags"),
|
||||
"user_location": result.get("user_location"),
|
||||
"time": result.get("time"),
|
||||
}
|
||||
|
||||
success = is_complete(result)
|
||||
|
||||
return {
|
||||
"success": success,
|
||||
"need_more_info": need_more_info,
|
||||
"result": out,
|
||||
"conversation_history": conversation_history,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=== Консольная версия ===")
|
||||
print("Введите ваш запрос (например: 'Хочу культурно провести время, потом попить кофе, я сейчас на улице Дальней около дома 8. У меня есть около двух часов')")
|
||||
user_input = input("> ").strip()
|
||||
response = parse_request(user_input=user_input)
|
||||
print(json.dumps(response, ensure_ascii=False, indent=2))
|
||||
164
input-to-route/ouput_chat.py
Normal file
164
input-to-route/ouput_chat.py
Normal file
@@ -0,0 +1,164 @@
|
||||
from typing import List, Dict, Optional
|
||||
from gemini_model import GeminiModel
|
||||
|
||||
|
||||
class RouteDescriber:
|
||||
"""
|
||||
Класс для работы с описанием маршрутов и ответами на вопросы пользователей через GeminiModel.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.model = GeminiModel()
|
||||
self.conversation_history = []
|
||||
self.route_points = []
|
||||
self.route_description = ""
|
||||
self.parser_chat_context = ""
|
||||
|
||||
def _call_model(self, prompt: str, system_instruction: Optional[str] = None) -> str:
|
||||
"""
|
||||
Вызывает GeminiModel с составленным промптом и системной инструкцией.
|
||||
"""
|
||||
full_prompt = prompt
|
||||
if system_instruction:
|
||||
full_prompt = f"Системная инструкция:\n{system_instruction}\n\n{prompt}"
|
||||
|
||||
return self.model.call(full_prompt)
|
||||
|
||||
def generate_route_description(self, route_points: List[Dict],
|
||||
parser_chat_history: Optional[List[Dict]] = None) -> str:
|
||||
"""
|
||||
Генерирует описание маршрута на основе точек.
|
||||
"""
|
||||
self.route_points = route_points
|
||||
self.conversation_history = []
|
||||
|
||||
# Формируем контекст парсера
|
||||
if parser_chat_history:
|
||||
print(parser_chat_history)
|
||||
parser_messages = [f"{msg['role'].upper()}: {msg['content']}" for msg in parser_chat_history]
|
||||
self.parser_chat_context = "\n".join(parser_messages)
|
||||
else:
|
||||
self.parser_chat_context = ""
|
||||
|
||||
# Формируем промпт с точками маршрута
|
||||
points_info = []
|
||||
for i, point in enumerate(route_points, 1):
|
||||
name = point.get('title', f'Точка {i}')
|
||||
description = point.get('description', '')
|
||||
points_info.append(f"{i}. {name}\nОписание: {description}")
|
||||
|
||||
prompt = f"""Создай краткое описание маршрута из {len(route_points)} точек.
|
||||
Для каждой точки напиши 1-2 предложения, которые заинтересуют посетителя.
|
||||
Используй только информацию из описаний точек.
|
||||
|
||||
Точки маршрута:
|
||||
{chr(10).join(points_info)}
|
||||
"""
|
||||
|
||||
if self.parser_chat_context:
|
||||
prompt = f"История запроса пользователя:\n{self.parser_chat_context}\n\n{prompt}"
|
||||
|
||||
system_instruction = """Ты — помощник для описания туристических маршрутов.
|
||||
Твоя задача — создавать краткие, привлекательные описания точек маршрута (1-2 предложения на точку).
|
||||
Используй только информацию, представленную в описаниях точек.
|
||||
|
||||
Пиши на русском языке."""
|
||||
|
||||
description = self._call_model(prompt, system_instruction)
|
||||
self.route_description = description
|
||||
return description
|
||||
|
||||
def answer_question(self, question: str) -> str:
|
||||
"""
|
||||
Отвечает на вопрос пользователя о точках маршрута.
|
||||
"""
|
||||
# Формируем контекст с информацией о точках
|
||||
points_context = [f"{i+1}. {p['name']}: {p.get('description', 'Нет описания')}"
|
||||
for i, p in enumerate(self.route_points)]
|
||||
context = "\n\n".join(points_context)
|
||||
|
||||
# Дополнительный контекст парсера и описания маршрута
|
||||
parser_info = f"\n\nИстория запроса пользователя:\n{self.parser_chat_context}" if self.parser_chat_context else ""
|
||||
route_desc_info = f"\n\nОписание маршрута:\n{self.route_description}" if self.route_description else ""
|
||||
|
||||
system_instruction = f"""Ты — помощник по туристическому маршруту.
|
||||
{parser_info}{route_desc_info}
|
||||
|
||||
Доступная информация о точках маршрута:
|
||||
{context}
|
||||
|
||||
СТРОГИЕ ПРАВИЛА:
|
||||
1. Отвечай ТОЛЬКО на вопросы о точках маршрута
|
||||
2. Используй ТОЛЬКО информацию из описаний точек выше
|
||||
3. Если в описаниях нет информации для ответа на вопрос, вежливо уходи от ответа фразами вида "Вы узнаете ответ, когда посетите это место", адаптируй под контекст.
|
||||
4. НЕ давай рекомендаций и советов
|
||||
5. НЕ отвечай на вопросы, не связанные с маршрутом. В таких случаях отвечай: "Я могу ответить только на вопросы о точках вашего маршрута"
|
||||
6. НЕ придумывай информацию, которой нет в описаниях
|
||||
7. Отвечай кратко и по существу на русском языке
|
||||
8. Ты видишь всю историю разговора, поэтому можешь отвечать на уточняющие вопросы"""
|
||||
|
||||
answer = self._call_model(question, system_instruction)
|
||||
return answer
|
||||
|
||||
def reset_conversation(self):
|
||||
"""Очищает историю разговора и маршрута."""
|
||||
self.conversation_history = []
|
||||
self.route_points = []
|
||||
self.route_description = ""
|
||||
self.parser_chat_context = ""
|
||||
|
||||
|
||||
|
||||
# Пример использования
|
||||
if __name__ == "__main__":
|
||||
# Инициализация (замените на ваш API ключ)
|
||||
API_KEY = "YOUR_GEMINI_API_KEY"
|
||||
describer = RouteDescriber()
|
||||
|
||||
# Пример истории чата с парсером
|
||||
parser_chat = [
|
||||
{"role": "user", "text": "Хочу погулять по центру Москвы, интересует история и искусство"},
|
||||
{"role": "model", "text": "Понял, вы хотите прогулку по историческому центру с посещением музеев. Сколько времени у вас есть?"},
|
||||
{"role": "user", "text": "Часа 3-4"},
|
||||
{"role": "model", "text": "Отлично, я составил маршрут из 3 точек по центру Москвы с историческими местами и музеем искусства."}
|
||||
]
|
||||
|
||||
# Пример данных маршрута
|
||||
route_points = [
|
||||
{
|
||||
"name": "Красная площадь",
|
||||
"description": "Главная площадь Москвы, окружённая историческими зданиями. Здесь находятся Кремль, собор Василия Блаженного и Мавзолей Ленина. Площадь была основана в конце XV века."
|
||||
},
|
||||
{
|
||||
"name": "Парк Горького",
|
||||
"description": "Один из самых известных парков Москвы, расположенный на берегу Москвы-реки. Здесь есть зоны для отдыха, спортивные площадки, прокат велосипедов. Парк был открыт в 1928 году."
|
||||
},
|
||||
{
|
||||
"name": "Третьяковская галерея",
|
||||
"description": "Художественный музей с крупнейшей коллекцией русского искусства. В экспозиции представлены работы от древнерусских икон до картин XX века. Основана купцом Павлом Третьяковым."
|
||||
}
|
||||
]
|
||||
|
||||
# Генерация описания маршрута с историей парсера
|
||||
print("=== ОПИСАНИЕ МАРШРУТА ===")
|
||||
description = describer.generate_route_description(route_points, parser_chat)
|
||||
print(description)
|
||||
print()
|
||||
|
||||
# Примеры вопросов (теперь с контекстом истории)
|
||||
print("=== ДИАЛОГ С ПОЛЬЗОВАТЕЛЕМ ===")
|
||||
|
||||
questions = [
|
||||
"Когда был основан Парк Горького?",
|
||||
"А что там можно делать?", # Уточняющий вопрос о предыдущей теме
|
||||
"Какие картины есть в Третьяковской галерее?",
|
||||
"Сколько стоит билет?", # Без контекста, но система поймет что это про музей
|
||||
"Расскажи про Кремль",
|
||||
"Что посоветуешь взять с собой?", # Попытка получить рекомендацию
|
||||
"Как погода сегодня?" # Нерелевантный вопрос
|
||||
]
|
||||
|
||||
for q in questions:
|
||||
print(f"\n👤 Пользователь: {q}")
|
||||
answer = describer.answer_question(q)
|
||||
print(f"🤖 Бот: {answer}")
|
||||
2576
input-to-route/output.json
Normal file
2576
input-to-route/output.json
Normal file
File diff suppressed because one or more lines are too long
7
input-to-route/places_from_xlsx.py
Normal file
7
input-to-route/places_from_xlsx.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import pandas as pd
|
||||
|
||||
# Читаем конкретный лист из Excel
|
||||
df = pd.read_excel('cultural_objects_mnn_final.xlsx', sheet_name='list1')
|
||||
|
||||
# Сохраняем в JSON с красивым форматированием
|
||||
df.to_json('output.json', orient='records', force_ascii=False, indent=4)
|
||||
17
input-to-route/pyproject.toml
Normal file
17
input-to-route/pyproject.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[project]
|
||||
name = "input-to-route"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"dotenv>=0.9.9",
|
||||
"fastapi>=0.120.2",
|
||||
"openai>=2.6.1",
|
||||
"openpyxl>=3.1.5",
|
||||
"pandas>=2.3.3",
|
||||
"pydantic>=2.12.3",
|
||||
"rapidfuzz>=3.14.1",
|
||||
"requests>=2.32.5",
|
||||
"uvicorn>=0.38.0",
|
||||
]
|
||||
286
input-to-route/requirements.txt
Normal file
286
input-to-route/requirements.txt
Normal file
@@ -0,0 +1,286 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv export --format requirements.txt
|
||||
annotated-doc==0.0.3 \
|
||||
--hash=sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580 \
|
||||
--hash=sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda
|
||||
# via fastapi
|
||||
annotated-types==0.7.0 \
|
||||
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
|
||||
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
|
||||
# via pydantic
|
||||
anyio==4.11.0 \
|
||||
--hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \
|
||||
--hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4
|
||||
# via
|
||||
# httpx
|
||||
# openai
|
||||
# starlette
|
||||
certifi==2025.10.5 \
|
||||
--hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \
|
||||
--hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43
|
||||
# via
|
||||
# httpcore
|
||||
# httpx
|
||||
# requests
|
||||
charset-normalizer==3.4.4 \
|
||||
--hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \
|
||||
--hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \
|
||||
--hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \
|
||||
--hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \
|
||||
--hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \
|
||||
--hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \
|
||||
--hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \
|
||||
--hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \
|
||||
--hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \
|
||||
--hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \
|
||||
--hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \
|
||||
--hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \
|
||||
--hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \
|
||||
--hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \
|
||||
--hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \
|
||||
--hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \
|
||||
--hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \
|
||||
--hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \
|
||||
--hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \
|
||||
--hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \
|
||||
--hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \
|
||||
--hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \
|
||||
--hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \
|
||||
--hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \
|
||||
--hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \
|
||||
--hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \
|
||||
--hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \
|
||||
--hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \
|
||||
--hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \
|
||||
--hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \
|
||||
--hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \
|
||||
--hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \
|
||||
--hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \
|
||||
--hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9
|
||||
# via requests
|
||||
click==8.3.0 \
|
||||
--hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
|
||||
--hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
|
||||
# via uvicorn
|
||||
colorama==0.4.6 ; sys_platform == 'win32' \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via
|
||||
# click
|
||||
# tqdm
|
||||
distro==1.9.0 \
|
||||
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
|
||||
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
|
||||
# via openai
|
||||
dotenv==0.9.9 \
|
||||
--hash=sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9
|
||||
# via input-to-route
|
||||
fastapi==0.120.2 \
|
||||
--hash=sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac \
|
||||
--hash=sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb
|
||||
# via input-to-route
|
||||
h11==0.16.0 \
|
||||
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
|
||||
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
|
||||
# via
|
||||
# httpcore
|
||||
# uvicorn
|
||||
httpcore==1.0.9 \
|
||||
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
|
||||
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
|
||||
# via httpx
|
||||
httpx==0.28.1 \
|
||||
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
|
||||
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
|
||||
# via openai
|
||||
idna==3.11 \
|
||||
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
|
||||
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
# requests
|
||||
jiter==0.11.1 \
|
||||
--hash=sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d \
|
||||
--hash=sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec \
|
||||
--hash=sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08 \
|
||||
--hash=sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94 \
|
||||
--hash=sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676 \
|
||||
--hash=sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe \
|
||||
--hash=sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe \
|
||||
--hash=sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c \
|
||||
--hash=sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f \
|
||||
--hash=sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96 \
|
||||
--hash=sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789 \
|
||||
--hash=sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a \
|
||||
--hash=sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646 \
|
||||
--hash=sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944 \
|
||||
--hash=sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991 \
|
||||
--hash=sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f \
|
||||
--hash=sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d \
|
||||
--hash=sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437 \
|
||||
--hash=sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8 \
|
||||
--hash=sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9 \
|
||||
--hash=sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663 \
|
||||
--hash=sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3 \
|
||||
--hash=sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a \
|
||||
--hash=sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc \
|
||||
--hash=sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea \
|
||||
--hash=sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14 \
|
||||
--hash=sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51 \
|
||||
--hash=sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c \
|
||||
--hash=sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f \
|
||||
--hash=sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58 \
|
||||
--hash=sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e \
|
||||
--hash=sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c \
|
||||
--hash=sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7 \
|
||||
--hash=sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111 \
|
||||
--hash=sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee \
|
||||
--hash=sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00 \
|
||||
--hash=sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd \
|
||||
--hash=sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b \
|
||||
--hash=sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90 \
|
||||
--hash=sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b \
|
||||
--hash=sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1 \
|
||||
--hash=sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9 \
|
||||
--hash=sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed \
|
||||
--hash=sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8
|
||||
# via openai
|
||||
openai==2.6.1 \
|
||||
--hash=sha256:27ae704d190615fca0c0fc2b796a38f8b5879645a3a52c9c453b23f97141bb49 \
|
||||
--hash=sha256:904e4b5254a8416746a2f05649594fa41b19d799843cd134dac86167e094edef
|
||||
# via input-to-route
|
||||
pydantic==2.12.3 \
|
||||
--hash=sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74 \
|
||||
--hash=sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf
|
||||
# via
|
||||
# fastapi
|
||||
# input-to-route
|
||||
# openai
|
||||
pydantic-core==2.41.4 \
|
||||
--hash=sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89 \
|
||||
--hash=sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d \
|
||||
--hash=sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2 \
|
||||
--hash=sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af \
|
||||
--hash=sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d \
|
||||
--hash=sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e \
|
||||
--hash=sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894 \
|
||||
--hash=sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa \
|
||||
--hash=sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e \
|
||||
--hash=sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1 \
|
||||
--hash=sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da \
|
||||
--hash=sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025 \
|
||||
--hash=sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5 \
|
||||
--hash=sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d \
|
||||
--hash=sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac \
|
||||
--hash=sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746 \
|
||||
--hash=sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a \
|
||||
--hash=sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84 \
|
||||
--hash=sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12 \
|
||||
--hash=sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2 \
|
||||
--hash=sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e \
|
||||
--hash=sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a \
|
||||
--hash=sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad \
|
||||
--hash=sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4 \
|
||||
--hash=sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf \
|
||||
--hash=sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0 \
|
||||
--hash=sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2 \
|
||||
--hash=sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d \
|
||||
--hash=sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2 \
|
||||
--hash=sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d \
|
||||
--hash=sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02 \
|
||||
--hash=sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616 \
|
||||
--hash=sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced \
|
||||
--hash=sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1 \
|
||||
--hash=sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c \
|
||||
--hash=sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4 \
|
||||
--hash=sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab \
|
||||
--hash=sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564 \
|
||||
--hash=sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554
|
||||
# via pydantic
|
||||
python-dotenv==1.2.1 \
|
||||
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
|
||||
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
|
||||
# via dotenv
|
||||
rapidfuzz==3.14.1 \
|
||||
--hash=sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a \
|
||||
--hash=sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b \
|
||||
--hash=sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548 \
|
||||
--hash=sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb \
|
||||
--hash=sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27 \
|
||||
--hash=sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef \
|
||||
--hash=sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a \
|
||||
--hash=sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e \
|
||||
--hash=sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9 \
|
||||
--hash=sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f \
|
||||
--hash=sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816 \
|
||||
--hash=sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6 \
|
||||
--hash=sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669 \
|
||||
--hash=sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c \
|
||||
--hash=sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a \
|
||||
--hash=sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f \
|
||||
--hash=sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000 \
|
||||
--hash=sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4 \
|
||||
--hash=sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd \
|
||||
--hash=sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b \
|
||||
--hash=sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d \
|
||||
--hash=sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8 \
|
||||
--hash=sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9 \
|
||||
--hash=sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f \
|
||||
--hash=sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260 \
|
||||
--hash=sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7 \
|
||||
--hash=sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25 \
|
||||
--hash=sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404 \
|
||||
--hash=sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c \
|
||||
--hash=sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc \
|
||||
--hash=sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598 \
|
||||
--hash=sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502 \
|
||||
--hash=sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4 \
|
||||
--hash=sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6 \
|
||||
--hash=sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9 \
|
||||
--hash=sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44 \
|
||||
--hash=sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f \
|
||||
--hash=sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440 \
|
||||
--hash=sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702 \
|
||||
--hash=sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1 \
|
||||
--hash=sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6
|
||||
# via input-to-route
|
||||
requests==2.32.5 \
|
||||
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
|
||||
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
|
||||
# via input-to-route
|
||||
sniffio==1.3.1 \
|
||||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||||
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
|
||||
# via
|
||||
# anyio
|
||||
# openai
|
||||
starlette==0.49.1 \
|
||||
--hash=sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb \
|
||||
--hash=sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875
|
||||
# via fastapi
|
||||
tqdm==4.67.1 \
|
||||
--hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \
|
||||
--hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2
|
||||
# via openai
|
||||
typing-extensions==4.15.0 \
|
||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via
|
||||
# fastapi
|
||||
# openai
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2 \
|
||||
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
|
||||
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
|
||||
# via pydantic
|
||||
urllib3==2.5.0 \
|
||||
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
|
||||
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
|
||||
# via requests
|
||||
uvicorn==0.38.0 \
|
||||
--hash=sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02 \
|
||||
--hash=sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d
|
||||
# via input-to-route
|
||||
470
input-to-route/route.py
Normal file
470
input-to-route/route.py
Normal file
@@ -0,0 +1,470 @@
|
||||
import math
|
||||
import itertools
|
||||
import requests
|
||||
from typing import List, Tuple, Dict, Optional, Set
|
||||
import random
|
||||
class Point:
|
||||
def __init__(self, coord: List[float], tag: str, visit_time: int, list_id=0):
|
||||
self.coord = coord
|
||||
self.tag = tag
|
||||
self.visit_time = visit_time
|
||||
self.matrix_index = None # Индекс точки в матрице расстояний
|
||||
self.estimated_time = None # Оценочное время (перемещение + посещение)
|
||||
self.list_id = list_id
|
||||
def haversine(coord1: List[float], coord2: List[float]) -> float:
|
||||
"""Calculate the great-circle distance between two points in kilometers."""
|
||||
lat1, lon1 = coord1
|
||||
lat2, lon2 = coord2
|
||||
R = 6371 # Earth radius in km
|
||||
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
a = (math.sin(dlat/2) * math.sin(dlat/2) +
|
||||
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
|
||||
math.sin(dlon/2) * math.sin(dlon/2))
|
||||
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
def filter_points_by_time(start_coord: List[float], points: List[Point], total_time: int) -> List[Point]:
|
||||
"""Filter points based on straight-line distance and visit time."""
|
||||
filtered = []
|
||||
for point in points:
|
||||
distance = haversine(start_coord, point.coord)
|
||||
travel_time = distance * 10 # Assume 6 km/h walking speed (10 min/km)
|
||||
point.estimated_time = travel_time + point.visit_time
|
||||
if point.estimated_time <= total_time:
|
||||
filtered.append(point)
|
||||
return filtered
|
||||
|
||||
def filter_points_by_tag_proximity(points: List[Point], max_per_tag: int = 10) -> List[Point]:
|
||||
"""For each tag, keep only the closest points (by estimated time)."""
|
||||
# Group points by tag
|
||||
tag_to_points = {}
|
||||
for point in points:
|
||||
if point.tag not in tag_to_points:
|
||||
tag_to_points[point.tag] = []
|
||||
tag_to_points[point.tag].append(point)
|
||||
|
||||
# For each tag, sort by estimated time and keep top max_per_tag
|
||||
filtered_points = []
|
||||
for tag, tag_points in tag_to_points.items():
|
||||
# Sort by estimated time (ascending)
|
||||
sorted_points = sorted(tag_points, key=lambda p: p.estimated_time)
|
||||
# Keep at most max_per_tag points
|
||||
kept_points = sorted_points[:max_per_tag]
|
||||
filtered_points.extend(kept_points)
|
||||
print(f"Tag '{tag}': kept {len(kept_points)} out of {len(tag_points)} points")
|
||||
|
||||
return filtered_points
|
||||
|
||||
def get_duration_matrix(points: List[List[float]]) -> Optional[Tuple[List[List[float]], List[List[float]]]]:
|
||||
"""Get duration matrix from server."""
|
||||
url = "https://ha1m-maap-pdmc.gw-1a.dockhost.net/table"
|
||||
payload = {"points": points}
|
||||
headers = {"content-type": "application/json"}
|
||||
|
||||
try:
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=30)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get("distances"), data.get("durations")
|
||||
else:
|
||||
print(f"Server error: {response.status_code}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Error requesting duration matrix: {e}")
|
||||
return None
|
||||
|
||||
def group_points_by_significance(points: List[Point], tag_importance: Dict[str, int]) -> Dict[int, List[Point]]:
|
||||
"""Group points by their importance level."""
|
||||
grouped = {}
|
||||
for point in points:
|
||||
importance = tag_importance.get(point.tag, float('inf'))[0]
|
||||
if importance not in grouped:
|
||||
grouped[importance] = []
|
||||
grouped[importance].append(point)
|
||||
return grouped
|
||||
|
||||
def calculate_route_time_with_matrix(route: List[Point], start_coord: List[float],
|
||||
duration_matrix: List[List[float]]) -> float:
|
||||
"""Calculate total time for a route using the duration matrix."""
|
||||
total_time = 0
|
||||
current_index = 0 # Start point index
|
||||
|
||||
for point in route:
|
||||
next_index = point.matrix_index
|
||||
travel_time_seconds = duration_matrix[current_index][next_index]
|
||||
travel_time_minutes = travel_time_seconds / 60.0
|
||||
total_time += travel_time_minutes + point.visit_time
|
||||
current_index = next_index
|
||||
|
||||
return total_time
|
||||
|
||||
def check_tags_constraint(points: List[Point]) -> bool:
|
||||
"""Check if there are no more than 5 unique tags."""
|
||||
unique_tags = set(point.tag for point in points)
|
||||
return len(unique_tags) <= 5
|
||||
|
||||
def generate_routes_exact_tags(grouped_points: Dict[int, List[Point]],
|
||||
all_tags: Set[str],
|
||||
tag_importance: Dict[str, int]) -> List[List[Point]]:
|
||||
"""Generate routes where each tag is visited exactly once using different coordinates."""
|
||||
# Create a mapping from tag to points
|
||||
tag_to_points = {}
|
||||
for points_list in grouped_points.values():
|
||||
for point in points_list:
|
||||
if point.tag not in tag_to_points:
|
||||
tag_to_points[point.tag] = []
|
||||
tag_to_points[point.tag].append(point)
|
||||
|
||||
# For each tag, we need to select exactly one point
|
||||
tag_selections = []
|
||||
for tag in all_tags:
|
||||
tag_selections.append(tag_to_points[tag])
|
||||
|
||||
# Generate all combinations of points (one per tag)
|
||||
all_routes = []
|
||||
print(len(list(itertools.product(*tag_selections))))
|
||||
for point_combination in itertools.product(*tag_selections):
|
||||
|
||||
# Check if all points have unique coordinates
|
||||
coords = [tuple(point.coord) for point in point_combination]
|
||||
if len(coords) != len(set(coords)):
|
||||
continue # Skip if any coordinates are duplicated
|
||||
|
||||
# Group points by importance
|
||||
points_by_importance = {}
|
||||
for point in point_combination:
|
||||
imp = tag_importance[point.tag][0]
|
||||
if imp not in points_by_importance:
|
||||
points_by_importance[imp] = []
|
||||
points_by_importance[imp].append(point)
|
||||
|
||||
# Sort by importance
|
||||
sorted_importances = sorted(points_by_importance.keys())
|
||||
|
||||
# Generate all permutations within each importance group
|
||||
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
|
||||
|
||||
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
|
||||
route = []
|
||||
for group in ordering:
|
||||
route.extend(group)
|
||||
all_routes.append(route)
|
||||
|
||||
return all_routes
|
||||
|
||||
def generate_routes_with_repeats(grouped_points: Dict[int, List[Point]],
|
||||
all_tags: Set[str],
|
||||
tag_importance: Dict[str, int],
|
||||
num_points: int) -> List[List[Point]]:
|
||||
"""Generate routes when we need to repeat tags to reach the required number of points, ensuring unique coordinates."""
|
||||
# Create a mapping from tag to points
|
||||
tag_to_points = {}
|
||||
for points_list in grouped_points.values():
|
||||
for point in points_list:
|
||||
if point.tag not in tag_to_points:
|
||||
tag_to_points[point.tag] = []
|
||||
tag_to_points[point.tag].append(point)
|
||||
|
||||
all_routes = []
|
||||
|
||||
# First, select one point for each tag (mandatory points)
|
||||
mandatory_selections = [tag_to_points[tag] for tag in all_tags]
|
||||
|
||||
# Generate all combinations of mandatory points (one per tag)
|
||||
for mandatory_combo in itertools.product(*mandatory_selections):
|
||||
mandatory_points = list(mandatory_combo)
|
||||
|
||||
# Check if mandatory points have unique coordinates
|
||||
mandatory_coords = [tuple(point.coord) for point in mandatory_points]
|
||||
if len(mandatory_coords) != len(set(mandatory_coords)):
|
||||
continue # Skip if any coordinates are duplicated in mandatory points
|
||||
|
||||
# We need to add (num_points - len(mandatory_points)) additional points
|
||||
num_additional = num_points - len(mandatory_points)
|
||||
|
||||
if num_additional == 0:
|
||||
# We have exactly the right number of points
|
||||
points_by_importance = {}
|
||||
for point in mandatory_points:
|
||||
imp = tag_importance[point.tag][0]
|
||||
if imp not in points_by_importance:
|
||||
points_by_importance[imp] = []
|
||||
points_by_importance[imp].append(point)
|
||||
|
||||
sorted_importances = sorted(points_by_importance.keys())
|
||||
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
|
||||
|
||||
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
|
||||
route = []
|
||||
for group in ordering:
|
||||
route.extend(group)
|
||||
all_routes.append(route)
|
||||
else:
|
||||
# We need to add additional points (can be from any tag, including repeats)
|
||||
# But we must ensure all coordinates are unique
|
||||
|
||||
# Get all available points excluding mandatory points
|
||||
all_available_points = []
|
||||
for key in grouped_points.keys():
|
||||
if tag_importance[key][1]:
|
||||
all_available_points.extend(grouped_points[key])
|
||||
|
||||
# Remove mandatory points from available points
|
||||
available_points = [p for p in all_available_points if p not in mandatory_points]
|
||||
|
||||
# Generate combinations of additional points
|
||||
for additional_combo in itertools.combinations(available_points, num_additional):
|
||||
# Check if additional points have unique coordinates and don't duplicate with mandatory
|
||||
additional_coords = [tuple(point.coord) for point in additional_combo]
|
||||
if len(additional_coords) != len(set(additional_coords)):
|
||||
continue # Skip if any coordinates are duplicated in additional points
|
||||
|
||||
# Check if additional points don't duplicate with mandatory points
|
||||
all_coords = mandatory_coords + additional_coords
|
||||
if len(all_coords) != len(set(all_coords)):
|
||||
continue # Skip if any coordinates are duplicated between mandatory and additional
|
||||
|
||||
full_route_candidate = mandatory_points + list(additional_combo)
|
||||
|
||||
# Group by importance
|
||||
points_by_importance = {}
|
||||
for point in full_route_candidate:
|
||||
imp = tag_importance[point.tag]
|
||||
if imp not in points_by_importance:
|
||||
points_by_importance[imp] = []
|
||||
points_by_importance[imp].append(point)
|
||||
|
||||
sorted_importances = sorted(points_by_importance.keys())
|
||||
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
|
||||
|
||||
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
|
||||
route = []
|
||||
for group in ordering:
|
||||
route.extend(group)
|
||||
all_routes.append(route)
|
||||
|
||||
return all_routes
|
||||
|
||||
def form_point_list(data):
|
||||
point_list =[]
|
||||
for list_id,entry in enumerate(data):
|
||||
point = Point(list(map(float,entry['coordinate'].split(', '))),entry['type'],entry['time_to_visit'],list_id)
|
||||
point_list.append(point)
|
||||
return point_list
|
||||
|
||||
def build_route(data, mapping,start_coord,total_time,n_nodes,strategy='best'):
|
||||
# Example input data - теперь с не более чем 5 уникальными тегами
|
||||
|
||||
start_coord_test = [56.331576, 44.003277]
|
||||
total_time_test = 180 # Увеличим время до 4 часов для большего выбора
|
||||
points = form_point_list(data)
|
||||
tag_importance =mapping
|
||||
# Используем 3 уникальных тега для демонстрации
|
||||
points_test = [
|
||||
Point([56.32448, 43.983546], "Памятник", 20),
|
||||
Point([56.335607, 43.97481], "Архитектура", 20),
|
||||
Point([56.313472, 43.990747], "Памятник", 20),
|
||||
#Point([56.324157, 44.002696], "Памятник", 20),
|
||||
#Point([56.316436, 43.994177], "Памятник", 20),
|
||||
#Point([56.32377, 44.001879], "Памятник", 20),
|
||||
#Point([56.329867, 43.99687], "Памятник", 20),
|
||||
Point([56.311066, 43.94595], "Памятник", 20),
|
||||
Point([56.333265, 43.972417], "Памятник", 20),
|
||||
# Point([56.332166, 44.012111], "Памятник", 20),
|
||||
#Point([56.326786, 44.006836], "Памятник", 20),
|
||||
Point([56.330232, 44.010941], "Парк", 20),
|
||||
Point([56.282221, 43.979263], "Парк", 20),
|
||||
Point([56.277315, 43.921408], "Мозаика", 20),
|
||||
Point([56.284829, 44.01893], "Парк", 20),
|
||||
Point([56.308973, 43.99821], "Парк", 20),
|
||||
Point([56.321545, 44.001921], "Парк", 20),
|
||||
#Point([56.301798, 44.044003], "Мозаика", 20),
|
||||
Point([56.268282, 43.919475], "Парк", 20),
|
||||
Point([56.239625, 43.854551], "Парк", 20),
|
||||
#Point([56.311214, 43.933981], "Парк", 20),
|
||||
Point([56.314984, 44.007347], "Парк", 20),
|
||||
Point([56.32509, 43.983433], "Парк", 20),
|
||||
Point([56.27449, 43.973357], "Парк", 20),
|
||||
Point([56.278073, 43.940886], "Парк", 20),
|
||||
Point([56.358805, 43.825376], "Парк", 20),
|
||||
Point([56.329995, 44.009444], "Памятник", 20),
|
||||
Point([56.328551, 43.998718], "Памятник", 20),
|
||||
Point([56.330355, 43.993105], "Архитектура", 20),
|
||||
Point([56.321416, 43.973897], "Архитектура", 20),
|
||||
# Point([56.327298, 44.005706], "Архитектура", 20),
|
||||
#Point([56.328757, 43.998183], "Архитектура", 20),
|
||||
# Point([56.328908, 43.995645], "Архитектура", 20),
|
||||
Point([56.317578, 43.995805], "Архитектура", 20),
|
||||
Point([56.329433, 44.012764], "Архитектура", 20),
|
||||
Point([56.3301, 44.008831], "Архитектура", 20),
|
||||
#Point([56.32995, 43.999495], "Архитектура", 20),
|
||||
Point([56.327454, 44.041745], "Архитектура", 20),
|
||||
#Point([56.328576, 44.004872], "Архитектура", 20),
|
||||
Point([56.3275, 44.007658], "Архитектура", 20),
|
||||
Point([56.330679, 44.013874], "Архитектура", 20),
|
||||
# Point([56.331541, 44.001747], "Архитектура", 20),
|
||||
# Point([56.335071, 43.974627], "Архитектура", 20),
|
||||
#Point([56.317707, 43.995847], "Архитектура", 20),
|
||||
#Point([56.323851, 43.985939], "Архитектура", 20),
|
||||
Point([56.325701, 44.001527], "Архитектура", 20),
|
||||
Point([56.328754, 43.998954], "Архитектура", 20),
|
||||
#Point([56.323937, 43.990728], "Музей", 20),
|
||||
#Point([56.2841, 43.84621], "Музей", 20),
|
||||
#Point([56.328646, 44.028973], "Музей", 20),
|
||||
Point([56.327391, 43.857522], "Мозаика", 20),
|
||||
#Point([56.252239, 43.889066], "Мозаика", 20),
|
||||
#Point([56.248436, 43.88106], "Мозаика", 20),
|
||||
#Point([56.321257, 43.94545], "Мозаика", 20),
|
||||
# Point([56.365284, 43.823251], "Мозаика", 20),
|
||||
Point([56.294371, 43.912625], "Мозаика", 20),
|
||||
#Point([56.241768, 43.859687], "Мозаика", 20),
|
||||
#Point([56.300073, 43.938526], "Мозаика", 20),
|
||||
#Point([56.229652, 43.947973], "Мозаика", 20),
|
||||
# Point([56.269486, 43.9238], "Мозаика", 20),
|
||||
Point([56.299251, 43.985146], "Мозаика", 20),
|
||||
Point([56.293297, 44.034095], "Мозаика", 20),
|
||||
Point([56.299251, 43.985146], "Мозаика", 20),
|
||||
Point([56.229652, 43.947973], "Мозаика", 20),
|
||||
Point([56.269486, 43.9238], "Мозаика", 20),
|
||||
#Point([56.293297, 44.034095], "Мозаика", 20),
|
||||
#Point([56.229652, 43.947973], "Мозаика", 20)
|
||||
]
|
||||
|
||||
tag_importance_test = {
|
||||
"Памятник": 1,
|
||||
"Парк": 1,
|
||||
"Мозаика": 1,
|
||||
"Архитектура": 1,
|
||||
#"Музей": 1
|
||||
}
|
||||
|
||||
# Check tags constraint
|
||||
if not check_tags_constraint(points):
|
||||
print("Error: More than 5 unique tags in the input data")
|
||||
return
|
||||
|
||||
print("Input data validation: OK (5 or fewer unique tags)")
|
||||
|
||||
# Step 1: Filter points using straight-line distance and total time
|
||||
filtered_by_time = filter_points_by_time(start_coord, points, total_time)
|
||||
print(f"After initial time filtering: {len(filtered_by_time)} points")
|
||||
|
||||
if len(filtered_by_time) < 3:
|
||||
print("Not enough points after time filtering")
|
||||
return
|
||||
|
||||
# Step 2: Filter points by tag proximity (keep max 10 closest points per tag)
|
||||
filtered_points = filter_points_by_tag_proximity(filtered_by_time, max_per_tag=10)
|
||||
print(f"After tag proximity filtering: {len(filtered_points)} points")
|
||||
|
||||
if len(filtered_points) < 3:
|
||||
print("Not enough points after tag proximity filtering")
|
||||
return
|
||||
|
||||
# Step 3: Prepare points for server request (start point + filtered points)
|
||||
points_for_matrix = [start_coord] + [point.coord for point in filtered_points]
|
||||
|
||||
print("Requesting duration matrix from server...")
|
||||
# Step 4: Get duration matrix from server
|
||||
result = get_duration_matrix(points_for_matrix)
|
||||
if result is None:
|
||||
print("Failed to get duration matrix from server")
|
||||
return
|
||||
|
||||
distances_matrix, durations_matrix = result
|
||||
print("Duration matrix received successfully")
|
||||
|
||||
# Assign matrix indices to points
|
||||
for i, point in enumerate(filtered_points):
|
||||
point.matrix_index = i + 1 # +1 because index 0 is the start point
|
||||
|
||||
# Step 5: Group by importance
|
||||
grouped_points = group_points_by_significance(filtered_points, tag_importance)
|
||||
|
||||
# Get all unique tags
|
||||
all_tags = set(point.tag for point in filtered_points)
|
||||
num_unique_tags = len(all_tags)
|
||||
|
||||
print(f"Unique tags: {all_tags} ({num_unique_tags} tags)")
|
||||
|
||||
# Step 6: Generate possible routes
|
||||
print("Generating possible routes...")
|
||||
|
||||
# Determine the number of points in the route
|
||||
if num_unique_tags >= n_nodes:
|
||||
# Each tag must be visited exactly once
|
||||
print("Each tag will be visited exactly once with unique coordinates")
|
||||
possible_routes = generate_routes_exact_tags(grouped_points, all_tags, tag_importance)
|
||||
else:
|
||||
# We have fewer than 3 unique tags, need to repeat some tags
|
||||
print(f"Only {num_unique_tags} unique tags available, will repeat tags to reach 3 points with unique coordinates")
|
||||
possible_routes = generate_routes_with_repeats(grouped_points, all_tags, tag_importance, n_nodes)
|
||||
|
||||
if not possible_routes:
|
||||
print("No valid routes found that cover all tags with unique coordinates")
|
||||
return
|
||||
|
||||
# Step 7: Calculate time for each route and filter by total_time
|
||||
valid_routes = []
|
||||
for route in possible_routes:
|
||||
route_time = calculate_route_time_with_matrix(route, start_coord, durations_matrix)
|
||||
if route_time <= total_time:
|
||||
valid_routes.append((route, route_time))
|
||||
|
||||
if not valid_routes:
|
||||
print("No valid routes found within time constraint")
|
||||
return
|
||||
|
||||
# Step 8: Find optimal route (minimum time)
|
||||
|
||||
if strategy=='random':
|
||||
optimal_route, min_time = random.choice(valid_routes)
|
||||
elif strategy=='longest':
|
||||
optimal_route, min_time = max(valid_routes, key=lambda x: x[1])
|
||||
else:
|
||||
optimal_route, min_time = min(valid_routes, key=lambda x: x[1])
|
||||
|
||||
print(f"\nOptimal route (time: {min_time:.2f} min):")
|
||||
for i, point in enumerate(optimal_route, 1):
|
||||
print(f"{i}. {point.tag} at {point.coord} ({point.visit_time} min)")
|
||||
|
||||
# Print route details with travel times
|
||||
print("\nRoute details:")
|
||||
current_index = 0
|
||||
total_route_time = 0
|
||||
for i, point in enumerate(optimal_route):
|
||||
travel_time_seconds = durations_matrix[current_index][point.matrix_index]
|
||||
travel_time_minutes = travel_time_seconds / 60.0
|
||||
segment_time = travel_time_minutes + point.visit_time
|
||||
total_route_time += segment_time
|
||||
|
||||
point.time_to_travel = travel_time_minutes
|
||||
|
||||
print(f"Segment {i+1}: {travel_time_minutes:.2f} min travel + {point.visit_time} min visit = {segment_time:.2f} min")
|
||||
current_index = point.matrix_index
|
||||
|
||||
print(f"Total route time: {total_route_time:.2f} min")
|
||||
|
||||
# Display all tags covered by the route
|
||||
route_tags = set(point.tag for point in optimal_route)
|
||||
#print(f"\nTags covered in this route: {', '.join(route_tags)}")
|
||||
if all_tags.issubset(route_tags):
|
||||
print("All tags are covered in this route!")
|
||||
|
||||
# Verify all coordinates are unique
|
||||
route_coords = [tuple(point.coord) for point in optimal_route]
|
||||
if len(route_coords) == len(set(route_coords)):
|
||||
print("All coordinates in the route are unique!")
|
||||
else:
|
||||
print("ERROR: Duplicate coordinates found in the route!")
|
||||
places = []
|
||||
for point in optimal_route:
|
||||
place = data[point.list_id].copy()
|
||||
place["time_to_travel"] = round(point.time_to_travel, 2)
|
||||
places.append(place)
|
||||
|
||||
return route_coords, places
|
||||
|
||||
#if __name__ == "__main__":
|
||||
# build_route()
|
||||
649
input-to-route/uv.lock
generated
Normal file
649
input-to-route/uv.lock
generated
Normal file
@@ -0,0 +1,649 @@
|
||||
version = 1
|
||||
revision = 2
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.10.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.9.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dotenv" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.120.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/fb/79e556bc8f9d360e5cc2fa7364a7ad6bda6f1736938b43a2791fa8baee7b/fastapi-0.120.2.tar.gz", hash = "sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac", size = 338684, upload-time = "2025-10-29T13:47:35.802Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/cc/1c33d05f62c9349bb80dfe789cc9a7409bdfb337a63fa347fd651d25294a/fastapi-0.120.2-py3-none-any.whl", hash = "sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb", size = 108383, upload-time = "2025-10-29T13:47:32.978Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input-to-route"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "dotenv" },
|
||||
{ name = "fastapi" },
|
||||
{ name = "openai" },
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pandas" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "rapidfuzz" },
|
||||
{ name = "requests" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "dotenv", specifier = ">=0.9.9" },
|
||||
{ name = "fastapi", specifier = ">=0.120.2" },
|
||||
{ name = "openai", specifier = ">=2.6.1" },
|
||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||
{ name = "pandas", specifier = ">=2.3.3" },
|
||||
{ name = "pydantic", specifier = ">=2.12.3" },
|
||||
{ name = "rapidfuzz", specifier = ">=3.14.1" },
|
||||
{ name = "requests", specifier = ">=2.32.5" },
|
||||
{ name = "uvicorn", specifier = ">=0.38.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/68/0357982493a7b20925aece061f7fb7a2678e3b232f8d73a6edb7e5304443/jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc", size = 168385, upload-time = "2025-10-17T11:31:15.186Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/4b/e4dd3c76424fad02a601d570f4f2a8438daea47ba081201a721a903d3f4c/jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663", size = 305272, upload-time = "2025-10-17T11:29:39.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/83/2cd3ad5364191130f4de80eacc907f693723beaab11a46c7d155b07a092c/jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94", size = 314038, upload-time = "2025-10-17T11:29:40.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/3c/8e67d9ba524e97d2f04c8f406f8769a23205026b13b0938d16646d6e2d3e/jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00", size = 345977, upload-time = "2025-10-17T11:29:42.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a5/489ce64d992c29bccbffabb13961bbb0435e890d7f2d266d1f3df5e917d2/jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd", size = 364503, upload-time = "2025-10-17T11:29:43.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/c0/e321dd83ee231d05c8fe4b1a12caf1f0e8c7a949bf4724d58397104f10f2/jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14", size = 487092, upload-time = "2025-10-17T11:29:44.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/5e/8f24ec49c8d37bd37f34ec0112e0b1a3b4b5a7b456c8efff1df5e189ad43/jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f", size = 376328, upload-time = "2025-10-17T11:29:46.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/70/ded107620e809327cf7050727e17ccfa79d6385a771b7fe38fb31318ef00/jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96", size = 356632, upload-time = "2025-10-17T11:29:47.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/53/c26f7251613f6a9079275ee43c89b8a973a95ff27532c421abc2a87afb04/jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c", size = 384358, upload-time = "2025-10-17T11:29:49.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/16/e0f2cc61e9c4d0b62f6c1bd9b9781d878a427656f88293e2a5335fa8ff07/jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646", size = 517279, upload-time = "2025-10-17T11:29:50.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/5c/4cd095eaee68961bca3081acbe7c89e12ae24a5dae5fd5d2a13e01ed2542/jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a", size = 508276, upload-time = "2025-10-17T11:29:52.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/25/f459240e69b0e09a7706d96ce203ad615ca36b0fe832308d2b7123abf2d0/jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b", size = 205593, upload-time = "2025-10-17T11:29:53.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/16/461bafe22bae79bab74e217a09c907481a46d520c36b7b9fe71ee8c9e983/jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed", size = 203518, upload-time = "2025-10-17T11:29:55.216Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/72/c45de6e320edb4fa165b7b1a414193b3cae302dd82da2169d315dcc78b44/jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d", size = 188062, upload-time = "2025-10-17T11:29:56.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/9b/4a57922437ca8753ef823f434c2dec5028b237d84fa320f06a3ba1aec6e8/jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b", size = 313814, upload-time = "2025-10-17T11:29:58.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/50/62a0683dadca25490a4bedc6a88d59de9af2a3406dd5a576009a73a1d392/jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58", size = 344987, upload-time = "2025-10-17T11:30:00.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/00/2355dbfcbf6cdeaddfdca18287f0f38ae49446bb6378e4a5971e9356fc8a/jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789", size = 356399, upload-time = "2025-10-17T11:30:02.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/07/c2bd748d578fa933d894a55bff33f983bc27f75fc4e491b354bef7b78012/jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec", size = 203289, upload-time = "2025-10-17T11:30:03.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/ee/ace64a853a1acbd318eb0ca167bad1cf5ee037207504b83a868a5849747b/jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8", size = 188284, upload-time = "2025-10-17T11:30:05.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/00/d6006d069e7b076e4c66af90656b63da9481954f290d5eca8c715f4bf125/jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676", size = 304624, upload-time = "2025-10-17T11:30:06.678Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/45/4a0e31eb996b9ccfddbae4d3017b46f358a599ccf2e19fbffa5e531bd304/jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944", size = 315042, upload-time = "2025-10-17T11:30:08.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/91/22f5746f5159a28c76acdc0778801f3c1181799aab196dbea2d29e064968/jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9", size = 346357, upload-time = "2025-10-17T11:30:10.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/4f/57620857d4e1dc75c8ff4856c90cb6c135e61bff9b4ebfb5dc86814e82d7/jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d", size = 365057, upload-time = "2025-10-17T11:30:11.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/34/caf7f9cc8ae0a5bb25a5440cc76c7452d264d1b36701b90fdadd28fe08ec/jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee", size = 487086, upload-time = "2025-10-17T11:30:13.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/17/85b5857c329d533d433fedf98804ebec696004a1f88cabad202b2ddc55cf/jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe", size = 376083, upload-time = "2025-10-17T11:30:14.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d3/2d9f973f828226e6faebdef034097a2918077ea776fb4d88489949024787/jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90", size = 357825, upload-time = "2025-10-17T11:30:15.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/55/848d4dabf2c2c236a05468c315c2cb9dc736c5915e65449ccecdba22fb6f/jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f", size = 383933, upload-time = "2025-10-17T11:30:17.34Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/6c/204c95a4fbb0e26dfa7776c8ef4a878d0c0b215868011cc904bf44f707e2/jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a", size = 517118, upload-time = "2025-10-17T11:30:18.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/25/09956644ea5a2b1e7a2a0f665cb69a973b28f4621fa61fc0c0f06ff40a31/jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3", size = 508194, upload-time = "2025-10-17T11:30:20.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/49/4d1657355d7f5c9e783083a03a3f07d5858efa6916a7d9634d07db1c23bd/jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea", size = 203961, upload-time = "2025-10-17T11:30:22.073Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/bd/f063bd5cc2712e7ca3cf6beda50894418fc0cfeb3f6ff45a12d87af25996/jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c", size = 202804, upload-time = "2025-10-17T11:30:23.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ca/4d84193dfafef1020bf0bedd5e1a8d0e89cb67c54b8519040effc694964b/jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991", size = 188001, upload-time = "2025-10-17T11:30:24.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/fa/3b05e5c9d32efc770a8510eeb0b071c42ae93a5b576fd91cee9af91689a1/jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c", size = 312561, upload-time = "2025-10-17T11:30:26.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/d3/335822eb216154ddb79a130cbdce88fdf5c3e2b43dc5dba1fd95c485aaf5/jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8", size = 344551, upload-time = "2025-10-17T11:30:28.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/6d/a0bed13676b1398f9b3ba61f32569f20a3ff270291161100956a577b2dd3/jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e", size = 363051, upload-time = "2025-10-17T11:30:30.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/03/313eda04aa08545a5a04ed5876e52f49ab76a4d98e54578896ca3e16313e/jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f", size = 485897, upload-time = "2025-10-17T11:30:31.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/13/a1011b9d325e40b53b1b96a17c010b8646013417f3902f97a86325b19299/jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9", size = 375224, upload-time = "2025-10-17T11:30:33.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/da/1b45026b19dd39b419e917165ff0ea629dbb95f374a3a13d2df95e40a6ac/jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08", size = 356606, upload-time = "2025-10-17T11:30:34.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/0c/9acb0e54d6a8ba59ce923a180ebe824b4e00e80e56cefde86cc8e0a948be/jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51", size = 384003, upload-time = "2025-10-17T11:30:35.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/2b/e5a5fe09d6da2145e4eed651e2ce37f3c0cf8016e48b1d302e21fb1628b7/jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437", size = 516946, upload-time = "2025-10-17T11:30:37.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/fe/db936e16e0228d48eb81f9934e8327e9fde5185e84f02174fcd22a01be87/jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111", size = 507614, upload-time = "2025-10-17T11:30:38.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/db/c4438e8febfb303486d13c6b72f5eb71cf851e300a0c1f0b4140018dd31f/jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7", size = 204043, upload-time = "2025-10-17T11:30:40.308Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/59/81badb169212f30f47f817dfaabf965bc9b8204fed906fab58104ee541f9/jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1", size = 204046, upload-time = "2025-10-17T11:30:41.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/01/43f7b4eb61db3e565574c4c5714685d042fb652f9eef7e5a3de6aafa943a/jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe", size = 188069, upload-time = "2025-10-17T11:30:43.23Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/44/303deb97be7c1c9b53118b52825cbd1557aeeff510f3a52566b1fa66f6a2/openai-2.6.1.tar.gz", hash = "sha256:27ae704d190615fca0c0fc2b796a38f8b5879645a3a52c9c453b23f97141bb49", size = 593043, upload-time = "2025-10-24T13:29:52.79Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/0e/331df43df633e6105ff9cf45e0ce57762bd126a45ac16b25a43f6738d8a2/openai-2.6.1-py3-none-any.whl", hash = "sha256:904e4b5254a8416746a2f05649594fa41b19d799843cd134dac86167e094edef", size = 1005551, upload-time = "2025-10-24T13:29:50.973Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "et-xmlfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "2.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "pytz" },
|
||||
{ name = "tzdata" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||
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, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rapidfuzz"
|
||||
version = "3.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/fc/a98b616db9a42dcdda7c78c76bdfdf6fe290ac4c5ffbb186f73ec981ad5b/rapidfuzz-3.14.1.tar.gz", hash = "sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c", size = 57869570, upload-time = "2025-09-08T21:08:15.922Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f2/0024cc8eead108c4c29337abe133d72ddf3406ce9bbfbcfc110414a7ea07/rapidfuzz-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260", size = 1926515, upload-time = "2025-09-08T21:06:39.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/ae/6cb211f8930bea20fa989b23f31ee7f92940caaf24e3e510d242a1b28de4/rapidfuzz-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4", size = 1388431, upload-time = "2025-09-08T21:06:41.73Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/88/bfec24da0607c39e5841ced5594ea1b907d20f83adf0e3ee87fa454a425b/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a", size = 1375664, upload-time = "2025-09-08T21:06:43.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/43/9f282ba539e404bdd7052c7371d3aaaa1a9417979d2a1d8332670c7f385a/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f", size = 1668113, upload-time = "2025-09-08T21:06:45.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/2f/0b3153053b1acca90969eb0867922ac8515b1a8a48706a3215c2db60e87c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b", size = 2212875, upload-time = "2025-09-08T21:06:47.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/9b/623001dddc518afaa08ed1fbbfc4005c8692b7a32b0f08b20c506f17a770/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f", size = 3161181, upload-time = "2025-09-08T21:06:49.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/b7/d8404ed5ad56eb74463e5ebf0a14f0019d7eb0e65e0323f709fe72e0884c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9", size = 1225495, upload-time = "2025-09-08T21:06:51.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6c/b96af62bc7615d821e3f6b47563c265fd7379d7236dfbc1cbbcce8beb1d2/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1", size = 2396294, upload-time = "2025-09-08T21:06:53.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/b7/c60c9d22a7debed8b8b751f506a4cece5c22c0b05e47a819d6b47bc8c14e/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25", size = 2529629, upload-time = "2025-09-08T21:06:55.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/94/a9ec7ccb28381f14de696ffd51c321974762f137679df986f5375d35264f/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502", size = 2782960, upload-time = "2025-09-08T21:06:57.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/80/04e5276d223060eca45250dbf79ea39940c0be8b3083661d58d57572c2c5/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f", size = 3298427, upload-time = "2025-09-08T21:06:59.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/63/24759b2a751562630b244e68ccaaf7a7525c720588fcc77c964146355aee/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e", size = 4267736, upload-time = "2025-09-08T21:07:01.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/a4/73f1b1f7f44d55f40ffbffe85e529eb9d7e7f7b2ffc0931760eadd163995/rapidfuzz-3.14.1-cp313-cp313-win32.whl", hash = "sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc", size = 1710515, upload-time = "2025-09-08T21:07:03.16Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/8b/a8fe5a6ee4d06fd413aaa9a7e0a23a8630c4b18501509d053646d18c2aa7/rapidfuzz-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d", size = 1540081, upload-time = "2025-09-08T21:07:05.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fe/4b0ac16c118a2367d85450b45251ee5362661e9118a1cef88aae1765ffff/rapidfuzz-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4", size = 812725, upload-time = "2025-09-08T21:07:07.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/cb/1ad9a76d974d153783f8e0be8dbe60ec46488fac6e519db804e299e0da06/rapidfuzz-3.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6", size = 1945173, upload-time = "2025-09-08T21:07:08.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/61/959ed7460941d8a81cbf6552b9c45564778a36cf5e5aa872558b30fc02b2/rapidfuzz-3.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd", size = 1413949, upload-time = "2025-09-08T21:07:11.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/a0/f46fca44457ca1f25f23cc1f06867454fc3c3be118cd10b552b0ab3e58a2/rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef", size = 1760666, upload-time = "2025-09-08T21:07:12.884Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/d0/7a5d9c04446f8b66882b0fae45b36a838cf4d31439b5d1ab48a9d17c8e57/rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8", size = 1579760, upload-time = "2025-09-08T21:07:14.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/aa/2c03ae112320d0746f2c869cae68c413f3fe3b6403358556f2b747559723/rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c", size = 832088, upload-time = "2025-09-08T21:07:17.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/36/53debca45fbe693bd6181fb05b6a2fd561c87669edb82ec0d7c1961a43f0/rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44", size = 1926336, upload-time = "2025-09-08T21:07:18.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/32/b874f48609665fcfeaf16cbaeb2bbc210deef2b88e996c51cfc36c3eb7c3/rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27", size = 1389653, upload-time = "2025-09-08T21:07:20.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/25/f6c5a1ff4ec11edadacb270e70b8415f51fa2f0d5730c2c552b81651fbe3/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6", size = 1380911, upload-time = "2025-09-08T21:07:22.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/f3/d322202ef8fab463759b51ebfaa33228100510c82e6153bd7a922e150270/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548", size = 1673515, upload-time = "2025-09-08T21:07:24.417Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/b9/6b2a97f4c6be96cac3749f32301b8cdf751ce5617b1c8934c96586a0662b/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9", size = 2219394, upload-time = "2025-09-08T21:07:26.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/bf/afb76adffe4406e6250f14ce48e60a7eb05d4624945bd3c044cfda575fbc/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6", size = 3163582, upload-time = "2025-09-08T21:07:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/34/e6405227560f61e956cb4c5de653b0f874751c5ada658d3532d6c1df328e/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9", size = 1221116, upload-time = "2025-09-08T21:07:30.8Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e6/5b757e2e18de384b11d1daf59608453f0baf5d5d8d1c43e1a964af4dc19a/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404", size = 2402670, upload-time = "2025-09-08T21:07:32.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c4/d753a415fe54531aa882e288db5ed77daaa72e05c1a39e1cbac00d23024f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816", size = 2521659, upload-time = "2025-09-08T21:07:35.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/28/d4e7fe1515430db98f42deb794c7586a026d302fe70f0216b638d89cf10f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f", size = 2788552, upload-time = "2025-09-08T21:07:37.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/00/eab05473af7a2cafb4f3994bc6bf408126b8eec99a569aac6254ac757db4/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a", size = 3306261, upload-time = "2025-09-08T21:07:39.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/31/2feb8dfcfcff6508230cd2ccfdde7a8bf988c6fda142fe9ce5d3eb15704d/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb", size = 4269522, upload-time = "2025-09-08T21:07:41.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/99/250538d73c8fbab60597c3d131a11ef2a634d38b44296ca11922794491ac/rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598", size = 1745018, upload-time = "2025-09-08T21:07:44.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/15/d50839d20ad0743aded25b08a98ffb872f4bfda4e310bac6c111fcf6ea1f/rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440", size = 1587666, upload-time = "2025-09-08T21:07:46.917Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ff/d73fec989213fb6f0b6f15ee4bbdf2d88b0686197951a06b036111cd1c7d/rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a", size = 835780, upload-time = "2025-09-08T21:07:49.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e7/f0a242687143cebd33a1fb165226b73bd9496d47c5acfad93de820a18fa8/rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669", size = 1945182, upload-time = "2025-09-08T21:07:51.84Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/29/ca8a3f8525e3d0e7ab49cb927b5fb4a54855f794c9ecd0a0b60a6c96a05f/rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702", size = 1413946, upload-time = "2025-09-08T21:07:53.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ef/6fd10aa028db19c05b4ac7fe77f5613e4719377f630c709d89d7a538eea2/rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7", size = 1795851, upload-time = "2025-09-08T21:07:55.76Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/30/acd29ebd906a50f9e0f27d5f82a48cf5e8854637b21489bd81a2459985cf/rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000", size = 1626748, upload-time = "2025-09-08T21:07:58.166Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/f4/dfc7b8c46b1044a47f7ca55deceb5965985cff3193906cb32913121e6652/rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b", size = 853771, upload-time = "2025-09-08T21:08:00.816Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[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, upload-time = "2024-02-25T23:20:04.057Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.49.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2025.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.38.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
|
||||
]
|
||||
40
input-to-route/wrapper.py
Normal file
40
input-to-route/wrapper.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from database import search_database
|
||||
from geocoder import validate_address
|
||||
from new_parser_test import parse_request
|
||||
from route import build_route
|
||||
from ouput_chat import RouteDescriber
|
||||
import json
|
||||
|
||||
if __name__=='__main__':
|
||||
parsed = parse_request(input())
|
||||
user_input, conversation_history = parsed['result'], parsed['conversation_history'],
|
||||
#user_input = json.loads(user_input)
|
||||
print(user_input)
|
||||
query =user_input['tags']
|
||||
user_address =user_input['user_location']
|
||||
user_time =user_input['time']
|
||||
val_output = validate_address('addresses.sqlite',user_address)
|
||||
print(val_output)
|
||||
found_points,mapping = search_database('output.json', query)
|
||||
print(len(found_points))
|
||||
print(mapping)
|
||||
user_position =[]
|
||||
if val_output['valid']:
|
||||
user_position.append(val_output['coordinates']['lat'])
|
||||
user_position.append(val_output['coordinates']['lon'])
|
||||
else:
|
||||
print('Адрес не найден')
|
||||
n_nodes = len(mapping)
|
||||
allow_extend = any(v[1] for v in mapping.values())
|
||||
print(len(mapping),allow_extend)
|
||||
route_otp =0
|
||||
if allow_extend:
|
||||
while n_nodes <= 5 and (route_otp := build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random') or route_otp): n_nodes += 1
|
||||
else:
|
||||
route_otp= build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random')
|
||||
route, places = route_otp
|
||||
dscb = RouteDescriber()
|
||||
print(conversation_history)
|
||||
description = dscb.generate_route_description(places,conversation_history)
|
||||
print(route)
|
||||
print(description)
|
||||
0
navigation-engine/.dockerignore
Normal file
0
navigation-engine/.dockerignore
Normal file
7
navigation-engine/Dockerfile
Normal file
7
navigation-engine/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM ghcr.io/project-osrm/osrm-backend:latest
|
||||
|
||||
WORKDIR /data
|
||||
|
||||
COPY data-final/ /data/
|
||||
|
||||
CMD ["osrm-routed", "--algorithm", "ch", "/data/nizhny.osrm"]
|
||||
BIN
navigation-engine/data-final/nizhny.osrm.cnbg
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.cnbg
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.cnbg_to_ebg
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.cnbg_to_ebg
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.datasource_names
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.datasource_names
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.ebg
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.ebg
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.ebg_nodes
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.ebg_nodes
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.edges
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.edges
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.enw
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.enw
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.fileIndex
Executable file
BIN
navigation-engine/data-final/nizhny.osrm.fileIndex
Executable file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.geometry
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.geometry
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.hsgr
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.hsgr
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.icd
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.icd
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.maneuver_overrides
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.maneuver_overrides
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.names
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.names
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.nbg_nodes
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.nbg_nodes
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.properties
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.properties
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.ramIndex
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.ramIndex
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.restrictions
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.restrictions
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.timestamp
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.timestamp
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.tld
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.tld
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.tls
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.tls
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.turn_duration_penalties
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.turn_duration_penalties
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.turn_penalties_index
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.turn_penalties_index
Normal file
Binary file not shown.
BIN
navigation-engine/data-final/nizhny.osrm.turn_weight_penalties
Normal file
BIN
navigation-engine/data-final/nizhny.osrm.turn_weight_penalties
Normal file
Binary file not shown.
2
web-ui/.env
Normal file
2
web-ui/.env
Normal file
@@ -0,0 +1,2 @@
|
||||
VITE_API_BASE_URL=https://xczl-a4zl-n2ko.gw-1a.dockhost.net
|
||||
VITE_OSRM_BASE_URL=https://8msn-80q0-el3y.gw-1a.dockhost.net
|
||||
35
web-ui/.gitignore
vendored
Normal file
35
web-ui/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# логи
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# зависимости
|
||||
node_modules/
|
||||
|
||||
# сборка
|
||||
dist/
|
||||
dist-ssr/
|
||||
|
||||
# локальные настройки
|
||||
*.local
|
||||
|
||||
# папки редакторов
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea/
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# временные/отладочные файлы
|
||||
*.tgz
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
3
web-ui/.vscode/extensions.json
vendored
Normal file
3
web-ui/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
37
web-ui/Dockerfile
Normal file
37
web-ui/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
# Stage 1: Build the Vue application
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
ARG VITE_API_BASE_URL
|
||||
ARG VITE_OSRM_BASE_URL
|
||||
|
||||
# Make them available as environment variables during build
|
||||
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
||||
ENV VITE_OSRM_BASE_URL=$VITE_OSRM_BASE_URL
|
||||
|
||||
# Copy package.json and package-lock.json (if it exists)
|
||||
# to leverage Docker cache for dependencies
|
||||
COPY package.json package-lock.json* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of the application code
|
||||
COPY . .
|
||||
|
||||
# Build the Vue application for production
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2: Serve the application with Nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy the built application from the builder stage to Nginx's default public directory
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Expose port 80, which is the default for Nginx
|
||||
EXPOSE 80
|
||||
|
||||
# Command to run Nginx in the foreground
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
5
web-ui/Makefile
Normal file
5
web-ui/Makefile
Normal file
@@ -0,0 +1,5 @@
|
||||
build_image:
|
||||
docker build -t gitea.private.nikidze.ru/nikidze/gorkycode-web-ui .
|
||||
|
||||
push_image:
|
||||
docker push gitea.private.nikidze.ru/nikidze/gorkycode-web-ui
|
||||
5
web-ui/README.md
Normal file
5
web-ui/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
13
web-ui/index.html
Normal file
13
web-ui/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Gorkycode: Туризм</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
2473
web-ui/package-lock.json
generated
Normal file
2473
web-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
web-ui/package.json
Normal file
27
web-ui/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "tourist-ai",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.16",
|
||||
"axios": "^1.13.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-vue-next": "^0.552.0",
|
||||
"marked": "^16.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"vite": "^7.1.7"
|
||||
}
|
||||
}
|
||||
1
web-ui/public/vite.svg
Normal file
1
web-ui/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg fill="#000000" height="200px" width="200px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 464.843 464.843" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M231.671,262.343c-15.991,0-29,13.01-29,29c0,15.99,13.009,29,29,29c15.991,0,29-13.01,29-29 C260.671,275.353,247.662,262.343,231.671,262.343z M231.671,312.343c-11.58,0-21-9.421-21-21s9.42-21,21-21 c11.58,0,21,9.421,21,21S243.251,312.343,231.671,312.343z"></path> <path d="M185.671,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C189.671,382.134,187.88,380.343,185.671,380.343z"></path> <path d="M208.921,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C212.921,382.134,211.13,380.343,208.921,380.343z"></path> <path d="M232.171,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C236.171,382.134,234.38,380.343,232.171,380.343z"></path> <path d="M255.421,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C259.421,382.134,257.63,380.343,255.421,380.343z"></path> <path d="M278.671,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4v-12 C282.671,382.134,280.88,380.343,278.671,380.343z"></path> <path d="M218.645,16.132l5.961,2.329l-3.454,5.388c-1.143,1.784-0.698,4.149,1.017,5.396c0.708,0.515,1.531,0.764,2.349,0.764 c1.16,0,2.308-0.503,3.096-1.464l4.057-4.95l4.057,4.95c0.799,0.974,1.978,1.453,3.143,1.464c2.196-0.016,3.971-1.801,3.971-4 c0-0.928-0.316-1.781-0.846-2.46l-3.261-5.087l5.961-2.329c1.974-0.77,3.003-2.947,2.349-4.962 c-0.655-2.015-2.765-3.162-4.817-2.634l-6.191,1.62l-0.373-6.39C235.541,1.652,233.79,0,231.671,0c-2.119,0-3.87,1.652-3.993,3.767 l-0.373,6.39l-6.191-1.62c-2.05-0.53-4.163,0.618-4.817,2.634C215.642,13.185,216.672,15.361,218.645,16.132z"></path> <path d="M397.987,360.278c-1.342-0.683-2.951-0.557-4.168,0.329l-10.688,7.773l-10.698-7.773c-1.217-0.884-2.826-1.012-4.167-0.328 c-1.34,0.684-2.184,2.061-2.184,3.565v26.5h-16.15v-26.5c0-1.505-0.844-2.882-2.185-3.565c-1.342-0.683-2.951-0.557-4.168,0.329 l-10.688,7.773l-10.688-7.773c-1.217-0.885-2.825-1.011-4.168-0.329c-1.34,0.683-2.184,2.06-2.184,3.565v26.5h-16.18v-59.5 c0-2.209-1.791-4-4-4h-2v-19.067c0-2.209-1.791-4-4-4c-2.209,0-4,1.791-4,4v19.067h-5.5v-75c0-2.209-1.791-4-4-4h-7v-91.5 c0-2.209-1.791-4-4-4h-7.959L235.602,37.851c-0.357-1.89-2.008-3.258-3.931-3.258c-1.923,0-3.574,1.368-3.931,3.258L206.13,152.343 h-7.959c-2.209,0-4,1.791-4,4v91.5h-7c-2.209,0-4,1.791-4,4v75h-6v-19.067c0-2.209-1.791-4-4-4s-4,1.791-4,4v19.067h-2 c-2.209,0-4,1.791-4,4v59.5h-14.18v-26.5c0-1.505-0.844-2.882-2.185-3.565c-1.341-0.683-2.951-0.557-4.168,0.329l-10.688,7.773 l-10.688-7.773c-1.216-0.885-2.826-1.011-4.168-0.329c-1.34,0.683-2.185,2.06-2.185,3.565v26.5h-16.15v-26.5 c0-1.504-0.844-2.881-2.184-3.565c-1.34-0.682-2.95-0.555-4.167,0.328l-10.697,7.773l-10.688-7.773 c-1.216-0.885-2.826-1.011-4.168-0.329c-1.34,0.683-2.185,2.06-2.185,3.565v97c0,2.209,1.791,4,4,4h327.5c2.209,0,4-1.791,4-4v-97 C400.171,362.338,399.327,360.961,397.987,360.278z M171.171,334.843h19.75v28.2c0,2.209,1.791,4,4,4s4-1.791,4-4v-28.2h28.5v28.2 c0,2.209,1.791,4,4,4s4-1.791,4-4v-28.2h28.5v28.2c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4v-28.2h19.75v85h-120.5V334.843z M261.171,215.343h-11v-55h11V215.343z M242.171,160.343v55h-21v-55H242.171z M231.671,60.159l17.4,92.184h-34.8L231.671,60.159z M213.171,160.343v55h-11v-55H213.171z M202.171,223.343h59v24.5h-59V223.343z M191.171,255.843h81v71h-81V255.843z M72.671,371.697l6.688,4.863c1.402,1.02,3.302,1.018,4.704,0.001l6.699-4.868v22.649c0,2.209,1.791,4,4,4h24.15 c2.209,0,4-1.791,4-4v-22.646l6.688,4.863c1.402,1.02,3.303,1.02,4.705,0l6.688-4.863v22.646c0,2.209,1.791,4,4,4h18.18v58.5h-90.5 V371.697z M171.171,456.843v-29h120.5v29H171.171z M392.171,456.843h-92.5v-58.5h20.18c2.209,0,4-1.791,4-4v-22.646l6.688,4.863 c1.402,1.02,3.303,1.02,4.705,0l6.688-4.863v22.646c0,2.209,1.791,4,4,4h24.15c2.209,0,4-1.791,4-4v-22.649l6.699,4.868 c1.402,1.018,3.302,1.019,4.704-0.001l6.688-4.863V456.843z"></path> <path d="M241.671,287.343h-6v-6.125c0-2.209-1.791-4-4-4s-4,1.791-4,4v10.125c0,2.209,1.791,4,4,4h10c2.209,0,4-1.791,4-4 C245.671,289.134,243.88,287.343,241.671,287.343z"></path> </g> </g></svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
81
web-ui/src/App.vue
Normal file
81
web-ui/src/App.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="w-full max-w-md mx-auto h-screen overflow-hidden flex flex-col bg-white">
|
||||
|
||||
<!-- Верхняя панель -->
|
||||
<div v-if="store.screen != 1 && store.screen != 6" class="flex justify-between items-center p-1 border-b border-gray-200">
|
||||
<!-- Кнопка Назад -->
|
||||
<button
|
||||
@click="goBack"
|
||||
class="rounded-full p-2 hover:bg-gray-100 transition disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
:disabled="store.screen === 1"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Кнопка Домой -->
|
||||
<button
|
||||
@click="goHome"
|
||||
class="rounded-full p-2 hover:bg-gray-100 transition"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0h6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Контейнер экранов -->
|
||||
<div class="flex-1 relative overflow-hidden">
|
||||
<Transition
|
||||
enter-from-class="transform translate-y-full"
|
||||
enter-active-class="transition-transform duration-500 ease-out"
|
||||
enter-to-class="transform translate-y-0"
|
||||
leave-from-class="transform translate-y-0"
|
||||
leave-active-class="transition-transform duration-500 ease-out"
|
||||
leave-to-class="transform -translate-y-full"
|
||||
>
|
||||
<component
|
||||
:is="currentScreen"
|
||||
:key="store.screen"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
/>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useMainStore } from "./store/main";
|
||||
import ScreenWelcome from "./components/ScreenWelcome.vue";
|
||||
import ScreenAddress from "./components/ScreenAddress.vue";
|
||||
import ScreenInterests from "./components/ScreenInterests.vue";
|
||||
import ScreenDuration from "./components/ScreenDuration.vue";
|
||||
import ScreenChat from "./components/ScreenChat.vue";
|
||||
import ScreenRoute from "./components/ScreenRoute.vue";
|
||||
|
||||
const store = useMainStore();
|
||||
|
||||
const currentScreen = computed(() => {
|
||||
return [
|
||||
ScreenWelcome,
|
||||
ScreenAddress,
|
||||
ScreenInterests,
|
||||
ScreenDuration,
|
||||
ScreenChat,
|
||||
ScreenRoute,
|
||||
][store.screen - 1];
|
||||
});
|
||||
|
||||
// Навигация
|
||||
function goBack() {
|
||||
if (store.screen > 1) {
|
||||
store.screen--;
|
||||
}
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
store.screen = 1;
|
||||
}
|
||||
</script>
|
||||
5
web-ui/src/assets/tailwind.css
Normal file
5
web-ui/src/assets/tailwind.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
:root {
|
||||
--btn: #06b6d4;
|
||||
}
|
||||
44
web-ui/src/components/ScreenAddress.vue
Normal file
44
web-ui/src/components/ScreenAddress.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<div class="p-5 border-b border-gray-100">
|
||||
<h2 class="text-xl font-semibold">Где ты находишься?</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Укажи адрес в свободной форме. Для точного поиска рекомендую ввести в формате: улица Такая-то, дом N.
|
||||
Приложение работает по Нижнему Новгороду.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-5">
|
||||
<input
|
||||
v-model="store.address"
|
||||
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
placeholder="улица Дальняя, дом 8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
<button
|
||||
:disabled="!store.address.trim()"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': !store.address.trim()
|
||||
}"
|
||||
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white py-3 rounded-2xl font-semibold transition"
|
||||
@click="goNext"
|
||||
>
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from "../store/main";
|
||||
|
||||
const store = useMainStore();
|
||||
|
||||
function goNext() {
|
||||
if (store.address.trim()) {
|
||||
store.screen = 3;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
181
web-ui/src/components/ScreenChat.vue
Normal file
181
web-ui/src/components/ScreenChat.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<div class="p-5 border-b border-gray-100">
|
||||
<h2 class="text-xl font-semibold">
|
||||
Уточняем детали маршрута
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-5" ref="messagesContainer">
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
:class="[
|
||||
'mb-4 p-3 rounded-lg max-w-[80%]',
|
||||
msg.role === 'user'
|
||||
? 'ml-auto bg-cyan-500 text-white'
|
||||
: 'mr-auto bg-gray-100 text-gray-800'
|
||||
]"
|
||||
>
|
||||
<p class="text-sm whitespace-pre-wrap">{{ msg.content }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="mb-4 p-3 rounded-lg max-w-[80%] mr-auto bg-gray-100 text-gray-800">
|
||||
<p class="text-sm">Думаю...</p>
|
||||
</div>
|
||||
|
||||
<div ref="bottomRef"></div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
v-model="userInput"
|
||||
@keyup.enter="sendMessage"
|
||||
:disabled="loading || routeBuilt"
|
||||
class="flex-1 border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500 disabled:bg-gray-100"
|
||||
placeholder="Напишите сообщение..."
|
||||
/>
|
||||
<button
|
||||
@click="sendMessage"
|
||||
:disabled="!userInput.trim() || loading || routeBuilt"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': !userInput.trim() || loading || routeBuilt
|
||||
}"
|
||||
class="bg-cyan-500 hover:bg-cyan-600 text-white px-6 rounded-lg font-semibold transition"
|
||||
>
|
||||
Отправить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, nextTick} from 'vue';
|
||||
import {useMainStore} from '../store/main';
|
||||
import axios from 'axios';
|
||||
|
||||
const store = useMainStore();
|
||||
|
||||
const messages = ref([]);
|
||||
const conversationHistory = ref([]);
|
||||
const userInput = ref('');
|
||||
const loading = ref(false);
|
||||
const routeBuilt = ref(false);
|
||||
const messagesContainer = ref(null);
|
||||
const bottomRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
// Отправляем первое сообщение
|
||||
const initialMessage = `Я нахожусь: ${store.address}. Мне интересно: ${store.interests}. У меня есть время: ${store.duration}`;
|
||||
|
||||
messages.value.push({
|
||||
role: 'user',
|
||||
content: initialMessage
|
||||
});
|
||||
|
||||
await scrollToBottom();
|
||||
await sendToServer(initialMessage);
|
||||
});
|
||||
|
||||
async function sendMessage() {
|
||||
if (!userInput.value.trim() || loading.value || routeBuilt.value) return;
|
||||
|
||||
const message = userInput.value.trim();
|
||||
userInput.value = '';
|
||||
|
||||
messages.value.push({
|
||||
role: 'user',
|
||||
content: message
|
||||
});
|
||||
|
||||
await scrollToBottom();
|
||||
await sendToServer(message);
|
||||
}
|
||||
|
||||
async function sendToServer(userMessage) {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const response = await axios.post(import.meta.env.VITE_API_BASE_URL+'/chat', {
|
||||
user_input: userMessage,
|
||||
conversation_history: conversationHistory.value
|
||||
}, {
|
||||
timeout: 60000 // 60 секунд
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
|
||||
// Обновляем историю разговора
|
||||
conversationHistory.value = data.conversation_history || [];
|
||||
|
||||
if (data.success && !data.need_more_info) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: data.result.description
|
||||
});
|
||||
|
||||
store.route = {
|
||||
coordinates: data.result.route,
|
||||
places: data.result.places,
|
||||
description: data.result.description,
|
||||
};
|
||||
|
||||
routeBuilt.value = true;
|
||||
await scrollToBottom();
|
||||
|
||||
setTimeout(() => {
|
||||
store.screen = 6;
|
||||
}, 2000);
|
||||
|
||||
} else if (data.need_more_info) {
|
||||
const lastMessage = data.conversation_history[data.conversation_history.length - 1];
|
||||
const assistantMessage = lastMessage?.content || 'Пожалуйста, уточните детали.';
|
||||
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: assistantMessage
|
||||
});
|
||||
|
||||
await scrollToBottom();
|
||||
|
||||
} else if (data.error) {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: data.error
|
||||
});
|
||||
|
||||
await scrollToBottom();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Chat error:', error);
|
||||
|
||||
// Проверка на таймаут
|
||||
if (error.code === 'ECONNABORTED') {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: 'Время ожидания истекло. Попробуйте еще раз.'
|
||||
});
|
||||
} else {
|
||||
messages.value.push({
|
||||
role: 'assistant',
|
||||
content: 'Произошла ошибка. Попробуйте еще раз.'
|
||||
});
|
||||
}
|
||||
|
||||
await scrollToBottom();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function scrollToBottom() {
|
||||
await nextTick();
|
||||
if (bottomRef.value) {
|
||||
bottomRef.value.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
45
web-ui/src/components/ScreenDuration.vue
Normal file
45
web-ui/src/components/ScreenDuration.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<div class="p-5 border-b border-gray-100">
|
||||
<h2 class="text-xl font-semibold">
|
||||
Сколько времени у тебя на прогулку?
|
||||
</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Укажи время в любом формате: «1 час», «90 минут», «полтора часа», «до 6 вечера» и т.д.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-5">
|
||||
<input
|
||||
v-model="store.duration"
|
||||
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
placeholder="2 часа"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-gray-100">
|
||||
<button
|
||||
:disabled="!store.duration || !store.duration.trim()"
|
||||
:class="{
|
||||
'opacity-50 cursor-not-allowed': !store.duration || !store.duration.trim()
|
||||
}"
|
||||
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white py-3 rounded-2xl font-semibold transition"
|
||||
@click="goNext"
|
||||
>
|
||||
Продолжить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {useMainStore} from "../store/main";
|
||||
|
||||
const store = useMainStore();
|
||||
|
||||
function goNext() {
|
||||
if (store.duration && store.duration.trim()) {
|
||||
store.screen = 5;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
58
web-ui/src/components/ScreenInterests.vue
Normal file
58
web-ui/src/components/ScreenInterests.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<div class="p-5">
|
||||
<h2 class="text-xl font-semibold">Как ты представляешь свой маршрут?</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
Опиши в свободной форме.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-5 overflow-y-auto">
|
||||
<textarea
|
||||
v-model="store.interests"
|
||||
@input="resizeTextarea"
|
||||
ref="textareaRef"
|
||||
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
rows="3"
|
||||
placeholder="Я хочу культурно провести время, а потом выпить кофе"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="p-4">
|
||||
<button
|
||||
class="w-full bg-cyan-500 text-white py-3 rounded-2xl font-semibold cursor-pointer"
|
||||
:class="{
|
||||
'hover:bg-cyan-600': isButtonEnabled,
|
||||
'opacity-50 cursor-not-allowed': !isButtonEnabled,
|
||||
}"
|
||||
@click="store.screen = 4"
|
||||
:disabled="!isButtonEnabled"
|
||||
>
|
||||
Далее
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, nextTick } from "vue";
|
||||
import { useMainStore } from "../store/main";
|
||||
const store = useMainStore();
|
||||
|
||||
const textareaRef = ref(null);
|
||||
|
||||
const isButtonEnabled = computed(() => {
|
||||
return store.interests.trim().length > 0;
|
||||
});
|
||||
|
||||
function resizeTextarea() {
|
||||
if (textareaRef.value) {
|
||||
textareaRef.value.style.height = "auto";
|
||||
textareaRef.value.style.height = textareaRef.value.scrollHeight + "px";
|
||||
}
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
resizeTextarea();
|
||||
});
|
||||
</script>
|
||||
286
web-ui/src/components/ScreenRoute.vue
Normal file
286
web-ui/src/components/ScreenRoute.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full bg-white">
|
||||
<!-- Шапка -->
|
||||
<div class="p-4 flex justify-between items-center border-b">
|
||||
<h2 class="text-lg font-semibold">Ваш маршрут готов</h2>
|
||||
<button
|
||||
class="text-sm text-cyan-600 cursor-pointer hover:underline"
|
||||
@click="restart"
|
||||
>
|
||||
Начать заново
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Основной контент с прокруткой -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<!-- Контейнер карты -->
|
||||
<div class="relative w-full h-64 rounded-lg overflow-hidden border">
|
||||
<div id="map" class="absolute inset-0"></div>
|
||||
</div>
|
||||
|
||||
<!-- Описание маршрута -->
|
||||
<div v-if="store.route?.description" class="prose prose-sm max-w-none">
|
||||
<div v-html="renderedDescription" class="text-gray-700 leading-relaxed"></div>
|
||||
</div>
|
||||
|
||||
<!-- Список точек маршрута -->
|
||||
<div>
|
||||
<h3 class="font-semibold text-lg mb-3">Точки маршрута</h3>
|
||||
<div class="space-y-5">
|
||||
<div v-for="(place, index) in store.route?.places" :key="index" class="border-b pb-4 last:border-b-0">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 bg-cyan-500 text-white rounded-full h-8 w-8 flex items-center justify-center font-bold">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h4 class="font-semibold text-md">{{ place.title || place.name }}</h4>
|
||||
<div class="text-xs text-gray-500 mt-1 flex items-center gap-4 flex-wrap">
|
||||
<span v-if="place.time_to_travel" class="flex items-center gap-1">
|
||||
<WalkIcon class="w-4 h-4" /> До точки: ~{{ Math.round(place.time_to_travel) }} мин.
|
||||
</span>
|
||||
<span v-if="place.time_to_visit" class="flex items-center gap-1">
|
||||
<HourglassIcon class="w-4 h-4" /> На точке: {{ place.time_to_visit }} мин.
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="place.explanation" class="text-sm text-gray-700 mt-2">
|
||||
{{ stripQuotes(place.explanation) }}
|
||||
</p>
|
||||
<p v-if="place.description" class="text-sm text-gray-700 mt-2">
|
||||
{{ place.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t">
|
||||
<button
|
||||
class="w-full border py-3 rounded-2xl cursor-pointer hover:bg-gray-50 transition"
|
||||
@click="restart"
|
||||
>
|
||||
Построить ещё маршрут
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, nextTick, computed } from "vue";
|
||||
import { useMainStore } from "../store/main";
|
||||
import { marked } from "marked";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { Home as HomeIcon, Footprints as WalkIcon, Hourglass as HourglassIcon } from "lucide-vue-next";
|
||||
|
||||
const store = useMainStore();
|
||||
let map;
|
||||
|
||||
const osrmBaseUrl = import.meta.env.VITE_OSRM_BASE_URL;
|
||||
|
||||
// Рендерим markdown в HTML
|
||||
const renderedDescription = computed(() => {
|
||||
if (!store.route?.description) return '';
|
||||
return marked(store.route.description);
|
||||
});
|
||||
|
||||
function stripQuotes(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/^"|"$/g, '');
|
||||
}
|
||||
|
||||
async function getRouteFromOSRM() {
|
||||
if (!osrmBaseUrl || !store.route?.places?.length) {
|
||||
console.warn("OSRM URL не сконфигурирован или нет точек для маршрута.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const allCoords = [
|
||||
store.route.coordinates[0],
|
||||
...store.route.places.map(p => {
|
||||
if (typeof p.coordinate === 'string') {
|
||||
return p.coordinate.split(',').map(Number);
|
||||
} else if (Array.isArray(p.coordinate)) {
|
||||
return p.coordinate;
|
||||
}
|
||||
return null;
|
||||
}).filter(c => c !== null)
|
||||
];
|
||||
|
||||
const coordsString = allCoords.map(coord => `${coord[1]},${coord[0]}`).join(';');
|
||||
|
||||
const url = `${osrmBaseUrl}/route/v1/foot/${coordsString}?overview=full&geometries=geojson`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`OSRM request failed with status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
|
||||
console.log("Маршрут от OSRM успешно получен.");
|
||||
return data.routes[0].geometry;
|
||||
} else {
|
||||
throw new Error(`OSRM response error: ${data.message || 'No route found'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ошибка при получении маршрута от OSRM:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function initMap() {
|
||||
if (!store.route?.coordinates) return;
|
||||
|
||||
const L = await import("leaflet");
|
||||
|
||||
await nextTick();
|
||||
|
||||
if (map) {
|
||||
map.remove();
|
||||
map = null;
|
||||
}
|
||||
|
||||
const container = document.getElementById("map");
|
||||
if (!container) return;
|
||||
|
||||
map = L.map(container, {
|
||||
zoomControl: false,
|
||||
attributionControl: false
|
||||
}).setView(store.route.coordinates[0], 14);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(map);
|
||||
|
||||
let routeLayer;
|
||||
|
||||
const routeGeometry = await getRouteFromOSRM();
|
||||
|
||||
if (routeGeometry) {
|
||||
routeLayer = L.geoJSON(routeGeometry, {
|
||||
style: {
|
||||
color: "#06b6d4",
|
||||
weight: 5,
|
||||
opacity: 0.8,
|
||||
}
|
||||
}).addTo(map);
|
||||
} else {
|
||||
console.warn("Не удалось построить маршрут через OSRM, рисуем прямые линии.");
|
||||
routeLayer = L.polyline(store.route.coordinates, {
|
||||
color: "#06b6d4",
|
||||
weight: 5,
|
||||
opacity: 0.8,
|
||||
}).addTo(map);
|
||||
}
|
||||
|
||||
store.route.places.forEach((place, i) => {
|
||||
let lat, lon;
|
||||
|
||||
if (typeof place.coordinate === 'string') {
|
||||
[lat, lon] = place.coordinate.split(',').map(Number);
|
||||
} else if (Array.isArray(place.coordinate)) {
|
||||
[lat, lon] = place.coordinate;
|
||||
} else {
|
||||
console.warn(`Неверный формат координат для точки ${i}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const customIcon = L.divIcon({
|
||||
className: 'custom-div-icon',
|
||||
html: `<div style='background-color:#06b6d4; color:white; border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; border: 2px solid white;'>${i + 1}</div>`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
|
||||
const popupContent = `<b>${place.title || place.name}</b>${place.address ? '<br>' + place.address : ''}`;
|
||||
|
||||
L.marker([lat, lon], {icon: customIcon})
|
||||
.addTo(map)
|
||||
.bindPopup(popupContent);
|
||||
});
|
||||
|
||||
const startIcon = L.divIcon({
|
||||
className: 'start-div-icon',
|
||||
html: `
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' style='color:#06b6d4;'>
|
||||
<path
|
||||
d='M3 9.75V21h6v-5.25a3 3 0 0 1 6 0V21h6V9.75L12 3 3 9.75z'
|
||||
fill='#06b6d4'
|
||||
stroke='white'
|
||||
stroke-width='2'
|
||||
stroke-linejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
`,
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 24]
|
||||
});
|
||||
|
||||
|
||||
L.marker(store.route.coordinates[0], {icon: startIcon})
|
||||
.addTo(map)
|
||||
.bindPopup("<b>Вы здесь</b><br>" + store.address);
|
||||
|
||||
map.fitBounds(routeLayer.getBounds().pad(0.1));
|
||||
|
||||
setTimeout(() => map.invalidateSize(), 350);
|
||||
}
|
||||
|
||||
onMounted(() => initMap());
|
||||
|
||||
function restart() {
|
||||
store.reset();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили для markdown контента */
|
||||
.prose :deep(h1) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose :deep(h2) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose :deep(h3) {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.prose :deep(p) {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.prose :deep(ul), .prose :deep(ol) {
|
||||
margin-left: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.prose :deep(li) {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.prose :deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.prose :deep(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.prose :deep(code) {
|
||||
background-color: #f3f4f6;
|
||||
padding: 0.125rem 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
35
web-ui/src/components/ScreenWelcome.vue
Normal file
35
web-ui/src/components/ScreenWelcome.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col justify-between bg-[#0b61a8] text-white">
|
||||
<div
|
||||
class="flex-1 flex flex-col items-center justify-center text-center p-6"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-2">
|
||||
Привет. Я ИИ-помощник туриста.
|
||||
</h1>
|
||||
<p class="mt-6 mb-6">Давай составим пешеходный маршрут по Нижнему Новгороду?</p>
|
||||
<div class="text-6xl mt-4 flex gap-4 justify-center">
|
||||
<PlaneIcon class="w-12 h-12" />
|
||||
<MapIcon class="w-12 h-12" />
|
||||
</div>
|
||||
<p class="mt-10">
|
||||
Я помогу подобрать прогулку — расскажи, где ты, что любишь и
|
||||
сколько времени хочешь потратить.
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<button
|
||||
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white font-semibold py-3 rounded-2xl cursor-pointer"
|
||||
@click="store.screen = 2"
|
||||
>
|
||||
Давай
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMainStore } from "../store/main";
|
||||
import { Plane as PlaneIcon, Map as MapIcon } from "lucide-vue-next";
|
||||
|
||||
const store = useMainStore();
|
||||
</script>
|
||||
8
web-ui/src/main.js
Normal file
8
web-ui/src/main.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './assets/tailwind.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
app.mount('#app')
|
||||
26
web-ui/src/store/main.js
Normal file
26
web-ui/src/store/main.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
export const useMainStore = defineStore('main', () => {
|
||||
const screen = ref(1)
|
||||
const address = ref('')
|
||||
const interests = ref('')
|
||||
const duration = ref('')
|
||||
const chat = reactive({ messages: [], asked: 0 })
|
||||
|
||||
function reset() {
|
||||
screen.value = 1
|
||||
address.value = ''
|
||||
duration.value = ''
|
||||
interests.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
screen,
|
||||
address,
|
||||
duration,
|
||||
interests,
|
||||
chat,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
11
web-ui/tailwind.config.js
Normal file
11
web-ui/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
10
web-ui/vite.config.js
Normal file
10
web-ui/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import {defineConfig} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
vue(),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user