FInished work

(Too lazy to split by commits)
This commit is contained in:
Dmitriy Shishkov 2023-09-21 20:41:56 +03:00
parent 600daa5498
commit c40a1b4f92
Signed by: dm1sh
GPG Key ID: 027994B0AA357688
30 changed files with 2424 additions and 144 deletions

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
.~lock*
**.mypy_cache/
**.venv/
**__pycache__/
*.docx
data*.csv

5
.env.example Normal file
View File

@ -0,0 +1,5 @@
REFETCH_PERIOD_H=6
POSTGRES_USER=rosseti
POSTGRES_PASSWORD=rosseti
POSTGRES_DB=rosseti
POSTGRES_HOST=db

15
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Module",
"type": "python",
"request": "launch",
"module": "parser",
"justMyCode": true,
}
]
}

7
Dockerfile Normal file
View File

@ -0,0 +1,7 @@
FROM python:3-slim
WORKDIR /srv
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade -r requirements.txt
COPY ./parser_api ./parser_api
COPY ./rosseti_parser ./rosseti_parser
CMD python -m uvicorn parser_api.main:app

View File

@ -1,82 +1,29 @@
**СПБ ГУП «ИАЦ»**
# СПБ ГУП «ИАЦ»
**Тестовое задание для Python-разработчика (дата аналитика)**
## Тестовое задание для 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)
Более подробно о нас: <https://iac.spb.ru/>
Модуль можно запустить командой `python -m rosseti_parser`, или импортировать из него необходимые методы и использовать где-то ещё
Мы на Хабре: <https://career.habr.com/companies/iac-spb>
- Анализ данных: [main.ipynb](https://git.dm1sh.ru/dm1sh/iac_test/src/branch/main/main.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)
Считывание таблицы с сайта <https://rosseti-lenenergo.ru/planned_work/>
FastAPI приложение, запускающее в дополнительном потоке периодическое обновление данных в базе.
![](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)
//При проблемах с парсингом для выполнения остальных задач можно
попросить готовый набор данных
Задания помеченные «**\***, **\*\***» будут оцениваться как
дополнительные.
Удачи!
Доступные методы:
- GET /api/list - Поиск по каждому полю в отдельности
- GET /api/search - Поиск по всем полям сразу
- GET /api/check - Проверка, является ли отключение в вашем доме сейчас официальным и если да, то когда сеть снова включат.
- PUT /api/create - Отладочное поле для добавления записей в БД
- GET / - Healthcheck

82
Task.md Normal file
View File

@ -0,0 +1,82 @@
**СПБ ГУП «ИАЦ»**
**Тестовое задание для 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)
//При проблемах с парсингом для выполнения остальных задач можно
попросить готовый набор данных
Задания помеченные «**\***, **\*\***» будут оцениваться как
дополнительные.
Удачи!

31
docker-compose.yml Normal file
View File

@ -0,0 +1,31 @@
version: "3"
services:
db:
image: postgres
ports:
- "5432:5432"
env_file:
- .env
volumes:
- db_storage:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"]
interval: 5s
timeout: 5s
retries: 5
hostname: "db"
app:
build: .
depends_on:
db:
condition: service_healthy
links:
- db
ports:
- "8000:8000"
env_file:
- .env
volumes:
db_storage:

1684
main.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,4 +0,0 @@
from .rosseti import RossetiParser
from .address import split_addresses
from .building_id import fetch_builing_ids
from .preprocess import preprocess_df

View File

@ -1,25 +0,0 @@
from __future__ import annotations
from typing import Optional, Tuple, Any
import requests
import pandas as pd
def get_building_id(row: pd.Series[Any]) -> Optional[Tuple[int, float, float]]:
r = requests.get('https://geocode.gate.petersburg.ru/parse/eas', params={
'street': row['Улица']
})
res = r.json()
if 'error' not in res:
return (res['Building_ID'], res['Longitude'], res['Latitude'])
return None
def fetch_builing_ids(df: pd.DataFrame) -> pd.DataFrame:
df[['Building_ID', 'lng', 'lat']] = df.apply(
get_building_id, axis=1, result_type='expand')
return df

View File

@ -1,19 +0,0 @@
import pandas as pd
def preprocess_df(df: pd.DataFrame) -> pd.DataFrame:
df['start'] = df['Плановая дата начала отключения электроснабжения'] + \
' ' + df['Плановое время начала отключения электроснабжения']
df['finish'] = df['Плановая дата восстановления отключения электроснабжения'] + \
' ' + df['Плановое время восстановления отключения электроснабжения']
df = df.drop(columns=[
'Улица',
'Плановая дата начала отключения электроснабжения',
'Плановая дата восстановления отключения электроснабжения',
'Плановое время начала отключения электроснабжения',
'Плановое время восстановления отключения электроснабжения'
])
return df

0
parser_api/__init__.py Normal file
View File

9
parser_api/config.py Normal file
View File

@ -0,0 +1,9 @@
import os
REFETCH_PERIOD_H = int(os.environ.get('REFETCH_PERIOD_H', '4'))
POSTGRES_USER = os.environ.get('POSTGRES_USER', 'rosseti')
POSTGRES_PASSWORD = os.environ.get('POSTGRES_PASSWORD', 'rosseti')
POSTGRES_DB = os.environ.get('POSTGRES_DB', 'rosseti')
POSTGRES_HOST = os.environ.get('POSTGRES_HOST', 'localhost')
POSTGRES_PORT = int(os.environ.get('POSTGRES_PORT', '5432'))

111
parser_api/controller.py Normal file
View File

@ -0,0 +1,111 @@
from typing import List, Optional
from functools import reduce
import datetime
from fastapi import HTTPException
from sqlalchemy import func, True_
from sqlalchemy.orm import Session
from sqlalchemy.sql import operators
from sqlalchemy.sql.expression import BinaryExpression
from . import models, schemas
def create_record(db: Session, record: schemas.Record):
db_record = models.Record(
region=record.region,
area=record.area,
town=record.town,
street=record.street,
start=record.start,
finish=record.finish,
branch=record.branch,
res=record.res,
comment=record.comment,
building_id=record.building_id,
lat=record.lat,
lng=record.lng,
)
db.add(db_record)
db.commit()
db.refresh(db_record)
return db_record
def contains_lower(name, val):
if type(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]):
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()
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))
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
}

31
parser_api/database.py Normal file
View File

@ -0,0 +1,31 @@
from typing import Generator
import os
from sqlalchemy import create_engine, URL
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
from .config import POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB
engine = create_engine(
URL.create(
"postgresql+psycopg",
username=POSTGRES_USER,
password=POSTGRES_PASSWORD,
host=POSTGRES_HOST,
port=POSTGRES_PORT,
database=POSTGRES_DB,
),
client_encoding='utf8',
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
# Dependency
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()

31
parser_api/job.py Normal file
View File

@ -0,0 +1,31 @@
from rosseti_parser import pipeline, preprocess_read_df
import pandas as pd
import numpy as np
from datetime import datetime
import logging
from .database import get_db
from . import models
from io import StringIO
def job():
fetch_start = datetime.now()
print("Starting refetch job: " + fetch_start.isoformat())
db = next(get_db())
parser = pipeline()
db.query(models.Record).delete()
db.commit()
print("Rewriting db: " + datetime.now().isoformat())
for i, row in parser.df.iterrows():
row = row.where((pd.notnull(row)), None)
db.add(models.Record(**row.to_dict()))
db.commit()
print(f"Fetched in {datetime.now() - fetch_start}\n{parser}")

37
parser_api/main.py Normal file
View File

@ -0,0 +1,37 @@
from contextlib import asynccontextmanager
import datetime
from fastapi import FastAPI
import schedule
from . import models, router
from .database import engine
from .scheduler import run_continuously, run_threaded
from .job import job
from .config import REFETCH_PERIOD_H
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('/')
def root():
return {
"up_since": start_stamp
}

23
parser_api/models.py Normal file
View File

@ -0,0 +1,23 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime, Float
from sqlalchemy.orm import relationship
from .database import Base
class Record(Base):
__tablename__ = 'records'
id = Column(Integer, primary_key=True, index=True)
index = Column(Integer)
region = Column(String, nullable=True)
area = Column(String, nullable=True)
town = Column(String, nullable=True)
street = Column(String, nullable=True)
start = Column(DateTime)
finish = Column(DateTime)
branch = Column(String, nullable=True)
res = Column(String, nullable=True)
comment = Column(String, nullable=True)
building_id = Column(Integer, nullable=True)
lat = Column(Float, nullable=True)
lng = Column(Float, nullable=True)

33
parser_api/router.py Normal file
View File

@ -0,0 +1,33 @@
from fastapi import HTTPException, Depends
from sqlalchemy.orm import Session
from typing import List, Annotated
from fastapi import APIRouter
from . import models, schemas, controller
from .database import SessionLocal, get_db
router = APIRouter(prefix='/api')
@router.get('/list', response_model=List[schemas.Record])
def list_rows(
filters: Annotated[schemas.RecordRequest, Depends()],
db: Session = Depends(get_db)
):
return controller.search_each(db, filters)
@router.get('/search', response_model=List[schemas.Record])
def search_rows(query: str, db: Session = Depends(get_db)):
return controller.search_all(db, query)
@router.get('/check', response_model=schemas.CheckResponse)
def check(building_id: int, db: Session = Depends(get_db)):
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)):
return controller.create_record(db, record)

25
parser_api/scheduler.py Normal file
View File

@ -0,0 +1,25 @@
import threading
import time
import schedule
def run_continuously(interval=1):
cease_continuous_run = threading.Event()
class ScheduleThread(threading.Thread):
@classmethod
def run(cls):
while not cease_continuous_run.is_set():
schedule.run_pending()
time.sleep(interval)
continuous_thread = ScheduleThread()
continuous_thread.start()
return cease_continuous_run.set
def run_threaded(job):
job_thread = threading.Thread(target=job)
job_thread.start()

39
parser_api/schemas.py Normal file
View File

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

72
requirements.dev.txt Normal file
View File

@ -0,0 +1,72 @@
annotated-types==0.5.0
anyio==3.7.1
asttokens==2.4.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
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
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

32
requirements.txt Normal file
View File

@ -0,0 +1,32 @@
annotated-types==0.5.0
anyio==3.7.1
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
greenlet==2.0.2
h11==0.14.0
idna==3.4
lxml==4.9.3
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

View File

@ -0,0 +1,5 @@
from .rosseti import RossetiParser
from .address import split_addresses
from .building_id import fetch_builing_ids
from .preprocess import preprocess_df, COL_NS, ICOL_NS, preprocess_read_df, group_by_index
from .util import pipeline

View File

@ -2,20 +2,11 @@ import sys
import schedule
import time
from . import RossetiParser, split_addresses, fetch_builing_ids, preprocess_df
from . import pipeline
def job() -> None:
parser = RossetiParser()
print(parser)
parser.df = split_addresses(parser.df)
parser.df = fetch_builing_ids(parser.df)
parser.df = preprocess_df(parser.df)
def job():
parser = pipeline()
parser.save_df(f'./data_{parser.today.strftime("%d-%m-%y_%H:%M")}.csv')

View File

@ -6,9 +6,9 @@ import re
T = TypeVar('T')
street_prefixes = ('ул.', 'бул.', 'пр.', 'ул', 'бул',
STREET_PREFIXES = ('ул.', 'бул.', 'пр.', 'ул', 'бул',
'пр', 'ш.', 'ш', 'пер.', 'пер')
houses_prefixes = ('д.', 'д')
HOUSES_PREFIXES = ('д.', 'д')
def unfold_house_ranges(token: str) -> str:
@ -56,8 +56,8 @@ def split_address(address: str) -> List[str]:
accumulator = ''
for i in range(len(tokens)):
if (any_of_in(street_prefixes, tokens[i].lower()) and
any_of_in(street_prefixes, accumulator.lower())):
if (any_of_in(STREET_PREFIXES, tokens[i].lower()) and
any_of_in(STREET_PREFIXES, accumulator.lower())):
res += unfold_houses_list(accumulator)
accumulator = ''
@ -71,17 +71,18 @@ def split_address(address: str) -> List[str]:
def process_row(row: pd.Series[str]) -> pd.Series[str]:
if pd.isnull(row['Улица']):
return row
addresses = split_address(row['Улица'])
row = row.copy()
row['Улица'] = addresses
if pd.isnull(row['Улица']):
row['Улица'] = [None]
else:
addresses = split_address(row['Улица'])
row['Улица'] = addresses
return row
def split_addresses(df: pd.DataFrame) -> pd.DataFrame:
return df.apply(process_row, axis=1).explode('Улица', ignore_index=True)
merged_df = df.apply(process_row, axis=1).reset_index()
return merged_df.explode('Улица', ignore_index=True)

View File

@ -0,0 +1,31 @@
from __future__ import annotations
from typing import Optional, Tuple, Any, List
import requests
import pandas as pd
import numpy as np
GeoTupleType = Tuple[Optional[int], Optional[float], Optional[float]]
def get_building_id(street: str) -> GeoTupleType:
if pd.isnull(street):
return None, None, None
r = requests.get('https://geocode.gate.petersburg.ru/parse/eas', params={
'street': street
}, timeout=10)
res = r.json()
if 'error' in res:
return None, None, None
return res['Building_ID'], res['Latitude'], res['Longitude']
def fetch_builing_ids(df: pd.DataFrame) -> pd.DataFrame:
df[['ID здания', 'Широта', 'Долгота']] = df.apply(
lambda row: get_building_id(row['Улица']), axis=1, result_type='expand')
return df

View File

@ -0,0 +1,62 @@
from __future__ import annotations
from typing import Any, List
import pandas as pd
COL_NS = {
'region': 'Регион РФ (область, край, город фед. значения, округ)',
'area': 'Административный район',
'town': 'Населённый пункт',
'street': 'Улица',
'start_date': 'Плановая дата начала отключения электроснабжения',
'start_time': 'Плановое время начала отключения электроснабжения',
'finish_date': 'Плановая дата восстановления отключения электроснабжения',
'finish_time': 'Плановое время восстановления отключения электроснабжения',
'branch': 'Филиал',
'res': 'РЭС',
'comment': 'Комментарий',
'building_id': 'ID здания',
'lat': 'Широта',
'lng': 'Долгота'
}
ICOL_NS = dict(map(reversed, COL_NS.items()))
def preprocess_df(df: pd.DataFrame) -> pd.DataFrame:
df.rename(columns=ICOL_NS, inplace=True)
for a in ('start', 'finish'):
df[f'{a}'] = pd.to_datetime(
df[f'{a}_date'].astype(str) + ' ' + df[f'{a}_time'].astype(str),
dayfirst=True
)
df.drop(columns=[f'{a}_date', f'{a}_time'], inplace=True)
return df
def preprocess_read_df(df: pd.DataFrame) -> pd.DataFrame:
for name in ('start', 'finish'):
df[name] = pd.to_datetime(df[name])
return df
def join_columns(col: pd.Series[Any]) -> List[Any] | Any:
first = col.iloc[0]
if col.name in ('street', 'building_id', 'lat', 'lng') and pd.notnull(first):
return list(col)
return first
def group_by_index(df: pd.DataFrame) -> pd.DataFrame:
groupped = df.groupby('index')
res_df = groupped.apply(
lambda index_df: index_df.apply(join_columns)
).drop(columns='index')
return res_df

18
rosseti_parser/util.py Normal file
View File

@ -0,0 +1,18 @@
from typing import Optional
from . import RossetiParser, split_addresses, fetch_builing_ids, preprocess_df
def pipeline(parser: Optional[RossetiParser] = None) -> RossetiParser:
if parser is None:
parser = RossetiParser()
print(parser)
parser.df = split_addresses(parser.df)
parser.df = fetch_builing_ids(parser.df)
parser.df = preprocess_df(parser.df)
return parser