Compare commits
No commits in common. "3741f984e92c39de1a7d699415fcd3e0cdc134f0" and "d53ffda0f2050f449c9cca8521b36d0e5c45124a" have entirely different histories.
3741f984e9
...
d53ffda0f2
@ -1,5 +1,5 @@
|
|||||||
REFETCH_PERIOD_H=6
|
REFETCH_PERIOD_H=6
|
||||||
POSTGRES_USER=lenenergo
|
POSTGRES_USER=rosseti
|
||||||
POSTGRES_PASSWORD=lenenergo
|
POSTGRES_PASSWORD=rosseti
|
||||||
POSTGRES_DB=lenenergo
|
POSTGRES_DB=rosseti
|
||||||
POSTGRES_HOST=db
|
POSTGRES_HOST=db
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
|
.~lock*
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
.venv
|
.venv
|
||||||
__pycache__
|
__pycache__
|
||||||
.env
|
.env
|
||||||
|
*.docx
|
||||||
data*.csv
|
data*.csv
|
@ -2,6 +2,6 @@ FROM python:3-slim
|
|||||||
WORKDIR /srv
|
WORKDIR /srv
|
||||||
COPY requirements.txt .
|
COPY requirements.txt .
|
||||||
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
||||||
COPY ./runner ./runner
|
COPY ./parser_api ./parser_api
|
||||||
COPY ./parser ./parser
|
COPY ./rosseti_parser ./rosseti_parser
|
||||||
CMD python -m runner
|
CMD python -m uvicorn parser_api.main:app
|
||||||
|
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# СПБ ГУП «ИАЦ»
|
||||||
|
|
||||||
|
## Тестовое задание для Python-разработчика (дата аналитика)
|
||||||
|
|
||||||
|
Задание: [Task.md](https://git.dm1sh.ru/dm1sh/iac_test/src/branch/main/Task.md)
|
||||||
|
|
||||||
|
- Скрипт для парсинга и работа с данными: [rosseti_parser](https://git.dm1sh.ru/dm1sh/iac_test/src/branch/main/rosseti_parser)
|
||||||
|
|
||||||
|
Модуль можно запустить командой `python -m rosseti_parser`, или импортировать из него необходимые методы и использовать где-то ещё
|
||||||
|
|
||||||
|
- Анализ данных: [analysis.ipynb](https://git.dm1sh.ru/dm1sh/iac_test/raw/branch/main/analysis.ipynb)
|
||||||
|
|
||||||
|
Необходимо скачать (платформа пока не поддерживает отображение ноутбука). Для запуска, установить библиотеки из `requirements.dev.txt`
|
||||||
|
|
||||||
|
- Визуализация: [Yandex DataLens](https://datalens.yandex/kq0ymfuy5w7e9)
|
||||||
|
|
||||||
|
Простенький дашборд с картой точек, хитмапом и несколькими графиками
|
||||||
|
|
||||||
|
- Создание базы данных, API и docker: [parser_api](https://git.dm1sh.ru/dm1sh/iac_test/src/branch/main/parser_api)
|
||||||
|
|
||||||
|
FastAPI приложение, запускающее в дополнительном потоке периодическое обновление данных в базе.
|
82
Task.md
Normal file
82
Task.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
**СПБ ГУП «ИАЦ»**
|
||||||
|
|
||||||
|
**Тестовое задание для Python-разработчика (дата аналитика)**
|
||||||
|
|
||||||
|
О компании:
|
||||||
|
|
||||||
|
Государственное унитарное предприятие, работающее в области
|
||||||
|
информатизации и информационного обеспечения органов государственной
|
||||||
|
власти Санкт-Петербурга и других организаций, а также предоставления
|
||||||
|
услуг в сфере создания и использования современных информационных и
|
||||||
|
телекоммуникационных систем, средств и технологий
|
||||||
|
|
||||||
|
Более подробно о нас: <https://iac.spb.ru/>
|
||||||
|
|
||||||
|
Мы на Хабре: <https://career.habr.com/companies/iac-spb>
|
||||||
|
|
||||||
|
**Текст задания:**
|
||||||
|
|
||||||
|
**- Парсинг**
|
||||||
|
|
||||||
|
Считывание таблицы с сайта <https://rosseti-lenenergo.ru/planned_work/>
|
||||||
|
|
||||||
|
{width="6.818295056867892in"
|
||||||
|
height="2.37117125984252in"}
|
||||||
|
|
||||||
|
Фильтр по времени - период текущей недели (текущий день и неделя вперед)
|
||||||
|
|
||||||
|
- Успешное чтение необходимых полей на сайте и их сохранение в
|
||||||
|
pandas.DataFrame
|
||||||
|
|
||||||
|
- Переключение между страницами и совершение полной выгрузки
|
||||||
|
{width="2.468441601049869in"
|
||||||
|
height="0.49993766404199474in"}
|
||||||
|
|
||||||
|
- Настройка автоматического запуска скрипта по расписанию
|
||||||
|
|
||||||
|
**- Работа с данными**
|
||||||
|
|
||||||
|
- Парсинг столбца Улица (разбиение строки на отдельные адреса)
|
||||||
|
|
||||||
|
- Геокод адресов через
|
||||||
|
<https://petersburg.ru/mainPortal/api_services/view/2223> и
|
||||||
|
сохранение building_id найденных зданий
|
||||||
|
|
||||||
|
- Запись результата в csv файл
|
||||||
|
|
||||||
|
**- Анализ данных и Визуализация**
|
||||||
|
|
||||||
|
- Выполнить в свободной форме на основе данных, полученных ранее
|
||||||
|
|
||||||
|
В дополнение к скрипту можно сделать дашборд на [Yandex DataLens
|
||||||
|
/Grafana](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&ved=2ahUKEwiCrdStlbKBAxViSPEDHcw0B4kQFnoECB0QAQ&url=https%3A%2F%2Fdatalens.yandex.ru%2F&usg=AOvVaw3gsVaz_KTvGMRtFZrsXAGk&opi=89978449)
|
||||||
|
|
||||||
|
**\*Создание базы данных**
|
||||||
|
|
||||||
|
- Вместо сохранения результата в csv, развернуть базу данных и
|
||||||
|
сохранять результаты в неё (PostgreSQL, Clickhouse, MongoDB)
|
||||||
|
|
||||||
|
- (Можно сделать скелет с подключением и записью в бд на локалхосте)
|
||||||
|
|
||||||
|
**\*API**
|
||||||
|
|
||||||
|
- Написать API к базе данных или csv-файлу на FastAPI
|
||||||
|
|
||||||
|
**\*\*Docker**
|
||||||
|
|
||||||
|
- Оборачивание всей сделанной работы в docker-compose
|
||||||
|
|
||||||
|
- При первоначальной настройке и запуске компоуза парсер начнет
|
||||||
|
работать и собирать данные в БД / csv. Доступ к данным
|
||||||
|
осуществляется по API.
|
||||||
|
|
||||||
|
Результаты проделанной работы залить на Github и прислать на
|
||||||
|
[tg:Faneagain](https://t.me/faneagain)
|
||||||
|
|
||||||
|
//При проблемах с парсингом для выполнения остальных задач можно
|
||||||
|
попросить готовый набор данных
|
||||||
|
|
||||||
|
Задания помеченные «**\***, **\*\***» будут оцениваться как
|
||||||
|
дополнительные.
|
||||||
|
|
||||||
|
Удачи!
|
1521
analysis.ipynb
Normal file
1521
analysis.ipynb
Normal file
File diff suppressed because one or more lines are too long
@ -1,4 +1,4 @@
|
|||||||
# Lenenergo parser
|
# Rosseti parser
|
||||||
|
|
||||||
## Описание
|
## Описание
|
||||||
|
|
||||||
@ -6,9 +6,9 @@
|
|||||||
|
|
||||||
## Интерфейс
|
## Интерфейс
|
||||||
|
|
||||||
- `LenenergoParser`:
|
- `RossetiParser`:
|
||||||
```python
|
```python
|
||||||
class LenenergoParser:
|
class RossetiParser:
|
||||||
def __init__(self, ndays=7, today: Optional[datetime] = None, file_path: Optional[str] = None) -> None
|
def __init__(self, ndays=7, today: Optional[datetime] = None, file_path: Optional[str] = None) -> None
|
||||||
|
|
||||||
self.base_url: str
|
self.base_url: str
|
||||||
@ -67,7 +67,7 @@ def group_by_index(df: pd.DataFrame) -> pd.DataFrame
|
|||||||
```
|
```
|
||||||
- `pipeline`:
|
- `pipeline`:
|
||||||
```python
|
```python
|
||||||
def pipeline(parser: Optional[LenenergoParser] = None) -> LenenergoParser
|
def pipeline(parser: Optional[RossetiParser] = None) -> RossetiParser
|
||||||
```
|
```
|
||||||
|
|
||||||
## Инструкция по запуску
|
## Инструкция по запуску
|
||||||
@ -79,7 +79,7 @@ python -m venv .venv
|
|||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
python -m parser [<Период в часах>]
|
python -m rosseti_parser [<Период в часах>]
|
||||||
```
|
```
|
||||||
|
|
||||||
Формат сохраняемых файлов: `data_%d-%m-%y_%H:%M.csv`
|
Формат сохраняемых файлов: `data_%d-%m-%y_%H:%M.csv`
|
||||||
|
@ -12,11 +12,11 @@ from .preprocess import (
|
|||||||
preprocess_df,
|
preprocess_df,
|
||||||
preprocess_read_df,
|
preprocess_read_df,
|
||||||
)
|
)
|
||||||
from .lenenergo import LenenergoParser
|
from .rosseti import RossetiParser
|
||||||
from .util import pipeline
|
from .util import pipeline
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
"LenenergoParser",
|
"RossetiParser",
|
||||||
"split_addresses",
|
"split_addresses",
|
||||||
"get_building_id",
|
"get_building_id",
|
||||||
"fetch_builing_ids",
|
"fetch_builing_ids",
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
LenenergoParser,
|
RossetiParser,
|
||||||
concurrent_fetch_builing_ids,
|
concurrent_fetch_builing_ids,
|
||||||
preprocess_df,
|
preprocess_df,
|
||||||
split_addresses,
|
split_addresses,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def pipeline(parser: Optional[LenenergoParser] = None) -> LenenergoParser:
|
def pipeline(parser: Optional[RossetiParser] = None) -> RossetiParser:
|
||||||
if parser is None:
|
if parser is None:
|
||||||
parser = LenenergoParser()
|
parser = RossetiParser()
|
||||||
|
|
||||||
print(parser)
|
print(parser)
|
||||||
|
|
||||||
|
79
requirements.dev.txt
Normal file
79
requirements.dev.txt
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
aiohttp==3.8.5
|
||||||
|
aiosignal==1.3.1
|
||||||
|
annotated-types==0.5.0
|
||||||
|
anyio==3.7.1
|
||||||
|
asttokens==2.4.0
|
||||||
|
async-timeout==4.0.3
|
||||||
|
attrs==23.1.0
|
||||||
|
autopep8==2.0.4
|
||||||
|
backcall==0.2.0
|
||||||
|
beautifulsoup4==4.12.2
|
||||||
|
certifi==2023.7.22
|
||||||
|
charset-normalizer==3.2.0
|
||||||
|
comm==0.1.4
|
||||||
|
contourpy==1.1.1
|
||||||
|
cycler==0.11.0
|
||||||
|
debugpy==1.8.0
|
||||||
|
decorator==5.1.1
|
||||||
|
executing==1.2.0
|
||||||
|
fastapi==0.103.1
|
||||||
|
fonttools==4.42.1
|
||||||
|
frozenlist==1.4.0
|
||||||
|
greenlet==2.0.2
|
||||||
|
idna==3.4
|
||||||
|
ipykernel==6.25.2
|
||||||
|
ipython==8.15.0
|
||||||
|
jedi==0.19.0
|
||||||
|
jupyter_client==8.3.1
|
||||||
|
jupyter_core==5.3.1
|
||||||
|
kiwisolver==1.4.5
|
||||||
|
lxml==4.9.3
|
||||||
|
matplotlib==3.8.0
|
||||||
|
matplotlib-inline==0.1.6
|
||||||
|
multidict==6.0.4
|
||||||
|
mypy==1.5.1
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
nest-asyncio==1.5.8
|
||||||
|
numpy==1.26.0
|
||||||
|
packaging==23.1
|
||||||
|
pandas==2.1.0
|
||||||
|
pandas-stubs==2.0.3.230814
|
||||||
|
parso==0.8.3
|
||||||
|
pexpect==4.8.0
|
||||||
|
pickleshare==0.7.5
|
||||||
|
Pillow==10.0.1
|
||||||
|
platformdirs==3.10.0
|
||||||
|
prompt-toolkit==3.0.39
|
||||||
|
psutil==5.9.5
|
||||||
|
ptyprocess==0.7.0
|
||||||
|
pure-eval==0.2.2
|
||||||
|
pycodestyle==2.11.0
|
||||||
|
pydantic==2.3.0
|
||||||
|
pydantic_core==2.6.3
|
||||||
|
Pygments==2.16.1
|
||||||
|
pyparsing==3.1.1
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
pytz==2023.3.post1
|
||||||
|
pyzmq==25.1.1
|
||||||
|
requests==2.31.0
|
||||||
|
schedule==1.2.0
|
||||||
|
scipy==1.11.2
|
||||||
|
seaborn==0.12.2
|
||||||
|
six==1.16.0
|
||||||
|
sniffio==1.3.0
|
||||||
|
soupsieve==2.5
|
||||||
|
SQLAlchemy==2.0.21
|
||||||
|
stack-data==0.6.2
|
||||||
|
starlette==0.27.0
|
||||||
|
tornado==6.3.3
|
||||||
|
traitlets==5.10.0
|
||||||
|
types-beautifulsoup4==4.12.0.6
|
||||||
|
types-html5lib==1.1.11.15
|
||||||
|
types-pytz==2023.3.1.0
|
||||||
|
types-requests==2.31.0.2
|
||||||
|
types-urllib3==1.26.25.14
|
||||||
|
typing_extensions==4.8.0
|
||||||
|
tzdata==2023.3
|
||||||
|
urllib3==2.0.4
|
||||||
|
wcwidth==0.2.6
|
||||||
|
yarl==1.9.2
|
@ -1,11 +1,39 @@
|
|||||||
aiohttp==3.8.5
|
aiohttp==3.8.5
|
||||||
|
aiosignal==1.3.1
|
||||||
|
annotated-types==0.5.0
|
||||||
|
anyio==3.7.1
|
||||||
|
async-timeout==4.0.3
|
||||||
|
attrs==23.1.0
|
||||||
beautifulsoup4==4.12.2
|
beautifulsoup4==4.12.2
|
||||||
pandas==2.1.1
|
bs4==0.0.1
|
||||||
Requests==2.31.0
|
certifi==2023.7.22
|
||||||
schedule==1.2.0
|
charset-normalizer==3.2.0
|
||||||
pandas==2.1.1
|
click==8.1.7
|
||||||
schedule==1.2.0
|
fastapi==0.103.1
|
||||||
SQLAlchemy==2.0.21
|
frozenlist==1.4.0
|
||||||
|
greenlet==2.0.2
|
||||||
|
h11==0.14.0
|
||||||
|
idna==3.4
|
||||||
lxml==4.9.3
|
lxml==4.9.3
|
||||||
psycopg==3.1.12
|
multidict==6.0.4
|
||||||
psycopg-binary==3.1.12
|
numpy==1.26.0
|
||||||
|
pandas==2.1.1
|
||||||
|
psycopg==3.1.10
|
||||||
|
psycopg-binary==3.1.10
|
||||||
|
psycopg-pool==3.1.7
|
||||||
|
pydantic==2.3.0
|
||||||
|
pydantic_core==2.6.3
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
pytz==2023.3.post1
|
||||||
|
requests==2.31.0
|
||||||
|
schedule==1.2.0
|
||||||
|
six==1.16.0
|
||||||
|
sniffio==1.3.0
|
||||||
|
soupsieve==2.5
|
||||||
|
SQLAlchemy==2.0.21
|
||||||
|
starlette==0.27.0
|
||||||
|
typing_extensions==4.8.0
|
||||||
|
tzdata==2023.3
|
||||||
|
urllib3==2.0.5
|
||||||
|
uvicorn==0.23.2
|
||||||
|
yarl==1.9.2
|
||||||
|
@ -1,4 +1,18 @@
|
|||||||
# Runner
|
# Parser API
|
||||||
|
|
||||||
|
## Описание
|
||||||
|
|
||||||
|
FastAPI REST API, предоставляющий доступ к данным с сайта [Россети Ленэнерго](https://rosseti-lenenergo.ru/planned_work/).
|
||||||
|
|
||||||
|
## Доступные методы
|
||||||
|
|
||||||
|
- GET `/api/list` - Поиск по каждому полю в отдельности
|
||||||
|
- GET `/api/search` - Поиск по всем полям сразу
|
||||||
|
- GET `/api/check` - Проверка, является ли отключение в вашем доме сейчас официальным и если да, то когда сеть снова включат.
|
||||||
|
- PUT `/api/create` - Отладочное поле для добавления записей в БД
|
||||||
|
- GET `/` - Healthcheck
|
||||||
|
|
||||||
|
Подробнее: [Swagger UI](http://localhost:8000/docs) когда запущенно приложение
|
||||||
|
|
||||||
## Инструкция по запуску
|
## Инструкция по запуску
|
||||||
|
|
||||||
@ -9,5 +23,5 @@ python -m venv .venv
|
|||||||
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
python -m runner
|
python -m uvicorn parser_api.main:app
|
||||||
```
|
```
|
@ -1,15 +0,0 @@
|
|||||||
import schedule
|
|
||||||
|
|
||||||
from . import models
|
|
||||||
from .config import REFETCH_PERIOD_H
|
|
||||||
from .database import engine
|
|
||||||
from .job import job
|
|
||||||
from .scheduler import run_continuously
|
|
||||||
|
|
||||||
models.Base.metadata.create_all(bind=engine)
|
|
||||||
|
|
||||||
schedule.every(REFETCH_PERIOD_H).hours.do(job)
|
|
||||||
stop_run_continuously = run_continuously()
|
|
||||||
|
|
||||||
# First run
|
|
||||||
job()
|
|
@ -2,8 +2,8 @@ import os
|
|||||||
|
|
||||||
REFETCH_PERIOD_H = int(os.environ.get("REFETCH_PERIOD_H", "4"))
|
REFETCH_PERIOD_H = int(os.environ.get("REFETCH_PERIOD_H", "4"))
|
||||||
|
|
||||||
POSTGRES_USER = os.environ.get("POSTGRES_USER", "lenenegro")
|
POSTGRES_USER = os.environ.get("POSTGRES_USER", "rosseti")
|
||||||
POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "lenenegro")
|
POSTGRES_PASSWORD = os.environ.get("POSTGRES_PASSWORD", "rosseti")
|
||||||
POSTGRES_DB = os.environ.get("POSTGRES_DB", "lenenegro")
|
POSTGRES_DB = os.environ.get("POSTGRES_DB", "rosseti")
|
||||||
POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "localhost")
|
POSTGRES_HOST = os.environ.get("POSTGRES_HOST", "localhost")
|
||||||
POSTGRES_PORT = int(os.environ.get("POSTGRES_PORT", "5432"))
|
POSTGRES_PORT = int(os.environ.get("POSTGRES_PORT", "5432"))
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
|
import datetime
|
||||||
|
from functools import reduce
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from parser import get_building_id
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.sql.expression import BinaryExpression
|
||||||
|
|
||||||
from . import models, schemas
|
from . import models, schemas
|
||||||
|
|
||||||
@ -23,3 +30,80 @@ def create_record(db: Session, record: schemas.Record):
|
|||||||
db.refresh(db_record)
|
db.refresh(db_record)
|
||||||
|
|
||||||
return db_record
|
return db_record
|
||||||
|
|
||||||
|
|
||||||
|
def contains_lower(name, val):
|
||||||
|
if isinstance(val, str):
|
||||||
|
return getattr(models.Record, name).icontains(val)
|
||||||
|
else:
|
||||||
|
return getattr(models.Record, name) == val
|
||||||
|
|
||||||
|
|
||||||
|
def and_if_can(a: BinaryExpression, b: Optional[BinaryExpression]) -> BinaryExpression:
|
||||||
|
if b is not None:
|
||||||
|
return a & b
|
||||||
|
else:
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def search_each(db: Session, filters: schemas.RecordRequest) -> List[schemas.Record]:
|
||||||
|
query = None
|
||||||
|
|
||||||
|
if filters.start:
|
||||||
|
query = models.Record.start <= filters.start
|
||||||
|
if filters.finish:
|
||||||
|
query = and_if_can(models.Record.finish >= filters.finish, query)
|
||||||
|
|
||||||
|
filters = list(
|
||||||
|
filter(lambda x: x[1] is not None and x[0] not in ("start, finish"), filters)
|
||||||
|
)
|
||||||
|
|
||||||
|
query = reduce(
|
||||||
|
lambda acc, ftr: and_if_can(contains_lower(*ftr), acc), filters, query
|
||||||
|
)
|
||||||
|
|
||||||
|
if query is None:
|
||||||
|
res = db.query(models.Record).all()
|
||||||
|
else:
|
||||||
|
res = db.query(models.Record).filter(query).all()
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def search_all(db: Session, prompt: str) -> List[schemas.Record]:
|
||||||
|
prompt = prompt.strip()
|
||||||
|
|
||||||
|
query = reduce(
|
||||||
|
lambda acc, name: acc | contains_lower(name, prompt),
|
||||||
|
("region", "area", "town", "street", "branch", "res"),
|
||||||
|
contains_lower("comment", prompt),
|
||||||
|
)
|
||||||
|
|
||||||
|
building_id, *_ = get_building_id(prompt)
|
||||||
|
|
||||||
|
if building_id is not None:
|
||||||
|
query |= models.Record.building_id == building_id
|
||||||
|
|
||||||
|
res = db.query(models.Record).filter(query).all()
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def check_outage(db: Session, building_id: int) -> schemas.CheckResponse:
|
||||||
|
building_query = db.query(models.Record).filter(
|
||||||
|
(models.Record.building_id == building_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if building_query.count() == 0:
|
||||||
|
raise HTTPException(404, "No such building")
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
res = building_query.filter(
|
||||||
|
(models.Record.start <= now) & (now <= models.Record.finish)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if res is None:
|
||||||
|
return {"is_outage": False}
|
||||||
|
|
||||||
|
return {"is_outage": True, "when_finish": res.finish}
|
||||||
|
35
runner/main.py
Normal file
35
runner/main.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import datetime
|
||||||
|
|
||||||
|
import schedule
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from . import models, router, schemas
|
||||||
|
from .config import REFETCH_PERIOD_H
|
||||||
|
from .database import engine
|
||||||
|
from .job import job
|
||||||
|
from .scheduler import run_continuously, run_threaded
|
||||||
|
|
||||||
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
start_stamp = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
schedule.every(REFETCH_PERIOD_H).hours.do(job)
|
||||||
|
stop_run_continuously = run_continuously()
|
||||||
|
|
||||||
|
run_threaded(job)
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
stop_run_continuously()
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(lifespan=lifespan)
|
||||||
|
|
||||||
|
app.include_router(router.router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", response_model=schemas.Healthcheck)
|
||||||
|
def Healthcheck():
|
||||||
|
return {"up_since": start_stamp}
|
74
runner/router.py
Normal file
74
runner/router.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
from typing import Annotated, List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from . import controller, schemas
|
||||||
|
from .database import get_db
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/list", response_model=List[schemas.Record], summary="Search by filters")
|
||||||
|
def list_rows(
|
||||||
|
filters: Annotated[schemas.RecordRequest, Depends()], db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Searches rows with specified filters.
|
||||||
|
|
||||||
|
Case insensitive contains:
|
||||||
|
- **region**
|
||||||
|
- **area**
|
||||||
|
- **town**
|
||||||
|
- **street**
|
||||||
|
- **start**
|
||||||
|
- **finish**
|
||||||
|
- **branch**
|
||||||
|
- **res**
|
||||||
|
- **comment**
|
||||||
|
Exact match:
|
||||||
|
- **index**
|
||||||
|
- **building_id**
|
||||||
|
- **lat**
|
||||||
|
- **lng**
|
||||||
|
Later or earlier than respectively:
|
||||||
|
- **start**
|
||||||
|
- **finish**
|
||||||
|
"""
|
||||||
|
return controller.search_each(db, filters)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/search", response_model=List[schemas.Record], summary="Search by query")
|
||||||
|
def search_rows(query: str, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Selects rows with cells containing case insensitive prompt as its part.
|
||||||
|
In addition, geocoding is being applied to prompt and if building_id found, corresponding row is being returned.
|
||||||
|
|
||||||
|
Rows to be searched:
|
||||||
|
- **region**
|
||||||
|
- **area**
|
||||||
|
- **town**
|
||||||
|
- **street**
|
||||||
|
- **branch**
|
||||||
|
- **res**
|
||||||
|
- **comment**
|
||||||
|
"""
|
||||||
|
return controller.search_all(db, query)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get(
|
||||||
|
"/check", response_model=schemas.CheckResponse, summary="Check when outage ends"
|
||||||
|
)
|
||||||
|
def check(building_id: int, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Checks if there is an active outage for building_id and if there is, also returns when will it end
|
||||||
|
"""
|
||||||
|
return controller.check_outage(db, building_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/create", response_model=schemas.Record)
|
||||||
|
def create_record(record: schemas.RecordCreate, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Not for public usage
|
||||||
|
"""
|
||||||
|
return controller.create_record(db, record)
|
@ -18,3 +18,8 @@ def run_continuously(interval=1):
|
|||||||
continuous_thread.start()
|
continuous_thread.start()
|
||||||
|
|
||||||
return cease_continuous_run.set
|
return cease_continuous_run.set
|
||||||
|
|
||||||
|
|
||||||
|
def run_threaded(job):
|
||||||
|
job_thread = threading.Thread(target=job)
|
||||||
|
job_thread.start()
|
||||||
|
43
runner/schemas.py
Normal file
43
runner/schemas.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRecord(BaseModel):
|
||||||
|
index: Optional[int] = None
|
||||||
|
region: Optional[str] = None
|
||||||
|
area: Optional[str] = None
|
||||||
|
town: Optional[str] = None
|
||||||
|
street: Optional[str] = None
|
||||||
|
branch: Optional[str] = None
|
||||||
|
res: Optional[str] = None
|
||||||
|
comment: Optional[str] = None
|
||||||
|
building_id: Optional[int] = None
|
||||||
|
lat: Optional[float] = None
|
||||||
|
lng: Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Record(BaseRecord):
|
||||||
|
id: int
|
||||||
|
start: datetime.datetime
|
||||||
|
finish: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
|
class RecordRequest(BaseRecord):
|
||||||
|
start: Optional[datetime.datetime] = None
|
||||||
|
finish: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class RecordCreate(BaseRecord):
|
||||||
|
start: datetime.datetime
|
||||||
|
finish: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
|
class CheckResponse(BaseModel):
|
||||||
|
is_outage: bool
|
||||||
|
when_finish: Optional[datetime.datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class Healthcheck(BaseModel):
|
||||||
|
up_since: datetime.datetime
|
Loading…
x
Reference in New Issue
Block a user