Deleted useless api code

This commit is contained in:
Dmitriy Shishkov 2023-10-11 20:42:51 +03:00
parent 4c8d8101f7
commit 620a6a820f
Signed by: dm1sh
GPG Key ID: 027994B0AA357688
12 changed files with 21 additions and 1980 deletions

2
.gitignore vendored
View File

@ -1,7 +1,5 @@
.~lock*
.mypy_cache
.venv
__pycache__
.env
*.docx
data*.csv

View File

@ -1,21 +0,0 @@
# СПБ ГУП «ИАЦ»
## Тестовое задание для 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
View File

@ -1,82 +0,0 @@
**СПБ ГУП «ИАЦ»**
**Тестовое задание для Python-разработчика (дата аналитика)**
О компании:
Государственное унитарное предприятие, работающее в области
информатизации и информационного обеспечения органов государственной
власти Санкт-Петербурга и других организаций, а также предоставления
услуг в сфере создания и использования современных информационных и
телекоммуникационных систем, средств и технологий 
Более подробно о нас: <https://iac.spb.ru/>
Мы на Хабре: <https://career.habr.com/companies/iac-spb>
**Текст задания:**
**- Парсинг**
Считывание таблицы с сайта <https://rosseti-lenenergo.ru/planned_work/>
![](media/image1.png){width="6.818295056867892in"
height="2.37117125984252in"}
Фильтр по времени - период текущей недели (текущий день и неделя вперед)
- Успешное чтение необходимых полей на сайте и их сохранение в
pandas.DataFrame
- Переключение между страницами и совершение полной выгрузки
![](media/image2.png){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)
//При проблемах с парсингом для выполнения остальных задач можно
попросить готовый набор данных
Задания помеченные «**\***, **\*\***» будут оцениваться как
дополнительные.
Удачи!

File diff suppressed because one or more lines are too long

View File

@ -1,79 +0,0 @@
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

View File

@ -1,39 +1,11 @@
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
bs4==0.0.1
certifi==2023.7.22
charset-normalizer==3.2.0
click==8.1.7
fastapi==0.103.1
frozenlist==1.4.0
greenlet==2.0.2
h11==0.14.0
idna==3.4
lxml==4.9.3
multidict==6.0.4
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
Requests==2.31.0
schedule==1.2.0
pandas==2.1.1
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
lxml==4.9.3
psycopg==3.1.12
psycopg-binary==3.1.12

15
runner/__main__.py Normal file
View File

@ -0,0 +1,15 @@
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()

View File

@ -1,11 +1,4 @@
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.sql.expression import BinaryExpression
from . import models, schemas
@ -30,80 +23,3 @@ def create_record(db: Session, record: schemas.Record):
db.refresh(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}

View File

@ -1,35 +0,0 @@
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}

View File

@ -1,74 +0,0 @@
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)

View File

@ -18,8 +18,3 @@ def run_continuously(interval=1):
continuous_thread.start()
return cease_continuous_run.set
def run_threaded(job):
job_thread = threading.Thread(target=job)
job_thread.start()

View File

@ -1,43 +0,0 @@
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