Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for hw4 #3

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
MODEL_PATH = "model.pkl"
DB_URL = "postgresql+psycopg2://postgres:postgres@postgres:5432/postgres"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.db
*.env
/venv
44 changes: 44 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
.DEFAULT_GOAL := help
PYTHONPATH = PYTHONPATH=./
TEST = $(PYTHONPATH) pytest --verbosity=2 --showlocals --log-level=DEBUG --strict-markers $(arg)
CODE = app tests

.PHONY: help
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

.PHONY: test
#test: ## Runs pytest with coverage
#$(TEST)

#.PHONY: test-fast
#test-fast: ## Runs pytest with exitfirst
# $(TEST) --exitfirst

#.PHONY: test-failed
#test-failed: ## Runs pytest from last-failed
# $(TEST) --last-failed

#.PHONY: test-cov
#test-cov: ## Runs pytest with coverage report
# $(TEST) --cov --cov-report html

.PHONY: lint
lint: ## Lint code
black --check $(CODE)
flake8 --jobs 4 --statistics --show-source --ignore E501,W503,E203 $(CODE)
pylint $(CODE)
mypy --namespace-packages --ignore-missing-imports --explicit-package-bases $(CODE)
# pytest --dead-fixtures --dup-fixtures
# safety check --full-report
# bandit -c pyproject.toml -r $(CODE)
poetry check

.PHONY: format
format: ## Formats all files
autoflake --recursive --in-place --ignore-init-module-imports --remove-all-unused-imports $(CODE)
isort $(CODE)
black $(CODE)

.PHONY: check
check: format lint #test ## Format and lint code then run tests
23 changes: 7 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
# 2024-spring-ab-python-ads-HW-4
## Интеграции и БД

## Интеграции и БД + повторение прошлого семестра :)
Здесь представлено решение задачи получения количества деревьев в определенном городе в определенном году с помощью Open Street Map.

* Реализовать с помощью FastAPI приложение с эндпоинтом (POST), позволяющим получить количество деревьев в определенном городе в определенном году с помощью Open Street Map.
API (см. example 3 здесь: https://wiki.openstreetmap.org/wiki/OSMPythonTools). Название города и страны + год должны передаваться в теле запроса.
* Для обращения к Open Street Map API использовать библиотеку requests, НЕ OSMPythonTools.
* Каждый успешный запрос должен логироваться в базу данных (postgresql).
* Реализовать хранимую процедуру в базе данных на plpython, вычисляющую город, который встретился наибольшее количество раз в истории обращений к эндпоинту.

Подсказка - Dockerfile для postgres с plpython лежит в репе.

**Критерии оценки**
* Реализован эндопинт - +2 балла
* Используется библиотека requests - +1 балл
* Реализован docker-compose с приложением и базой данных - +4 балла
* Реализована хранимая процедура - +2 балла
* Правильная структура репозитория - +1 балл
* Реализовано с помощью FastAPI приложение с эндпоинтом (POST), позволяющим получить количество деревьев в определенном городе в определенном году с помощью Open Street Map.
API.
* Для обращения к Open Street Map API используется библиотека requests.
* Каждый успешный запрос логируется в базу данных (postgresql).
* Реализована хранимая процедуру в базе данных, вычисляющая город, который встретился наибольшее количество раз в истории обращений к эндпоинту.
12 changes: 12 additions & 0 deletions app.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.11 as requirements-stage
WORKDIR /tmp
RUN pip install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org poetry
COPY ./pyproject.toml ./poetry.lock* /tmp/
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes

FROM python:3.11
WORKDIR /code
COPY --from=requirements-stage /tmp/requirements.txt /code/requirements.txt
RUN pip3 install --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"]
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""init file."""
16 changes: 16 additions & 0 deletions app/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Create database attributes."""

import os

from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker

load_dotenv()


engine = create_engine(os.getenv("DB_URL"))

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()
47 changes: 47 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""This is the fraud detector."""

from datetime import datetime
from typing import Any

from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session

from . import models, schemas
from .database import SessionLocal, engine
from .model_predict import tree_count

app = FastAPI()
models.Base.metadata.create_all(bind=engine)


def get_db():
"""Give database."""
db = SessionLocal()
try:
yield db
finally:
db.close()


@app.get("/")
async def root():
"""Hello message."""
return {"message": "Hello! This is an information application about trees."}


@app.post("/information")
async def post_information(
buff: schemas.InputFeatures, db: Session = Depends(get_db)
) -> int | Any:
"""Get info."""
info = tree_count(buff.city, buff.year)
new_detect = models.Trees(
city=buff.city,
country=buff.country,
year=int(buff.year),
trees_count=info,
request_time=datetime.now(),
)
db.add(new_detect)
db.commit()
return info
20 changes: 20 additions & 0 deletions app/model_predict.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Model predictions."""
import requests


def tree_count(town, year):
"""Parser for OSM."""
url = r"http://overpass-api.de/api/interpreter"
query = f"""[out:json][date:'{str(year)}-01-01T00:00:00Z'][maxsize:2000000000];
area["name:en"="{town}"]->.searchArea;
(node["natural"="tree"](area.searchArea);); out count;"""
result = requests.post(url, data=query, timeout=60)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут стоит обрабатывать ошибки.

t = result.text
cap = "total"
for i in range(len(t)):
if t[i : i + len(cap)] == cap:
t = t[i + len(cap) + 4 :]
for j in range(len(t)): # pylint: disable=C0200
if not t[j].isdigit():
return int(t[:j])
return 0
18 changes: 18 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""This program create template from database."""

from sqlalchemy import Column, DateTime, Integer, String

from .database import Base


class Trees(Base): # pylint: disable=R0903
"""Template from database."""

__tablename__ = "Trees"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше писать имена таблиц в нижнем регистре.


id = Column(Integer, primary_key=True)
city = Column(String, default="")
country = Column(String, default="")
year = Column(Integer, default=2000)
trees_count = Column(Integer, default=0)
request_time = Column(DateTime)
11 changes: 11 additions & 0 deletions app/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""All schemas."""

from pydantic import BaseModel


class InputFeatures(BaseModel):
"""Input features."""

city: str = ""
country: str = ""
year: str = ""
20 changes: 20 additions & 0 deletions database.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
CREATE EXTENSION IF NOT EXISTS plpython3u;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Зачем plpython3u, если используешь plpgsql ?


CREATE TABLE post_information (
id SERIAL PRIMARY KEY,
city VARCHAR(255),
country VARCHAR(255),
year INT,
trees_count INT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE OR REPLACE FUNCTION most_frequent_city() RETURNS TABLE(city VARCHAR, queries_count bigint) AS $$ BEGIN RETURN QUERY
SELECT t.city,
COUNT(*) as queries_count
FROM "Trees" t
GROUP BY t.city
ORDER BY queries_count DESC
LIMIT 1;
END;
$$ LANGUAGE plpgsql;
Binary file added detector.db
Binary file not shown.
27 changes: 27 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
version: "3.3"

services:
postgres:
build:
context: .
dockerfile: pg.Dockerfile
ports:
- 5432:5432
volumes:
- pgdata:/var/lib/postgresql/data/pgdata
environment:
- PGDATA=/var/lib/postgresql/data/pgdata
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres

app:
build:
context: .
dockerfile: app.Dockerfile
ports:
- 8000:80
environment:
- DB_URL=postgresql+psycopg2://postgres:postgres@postgres:5432/postgres
volumes:
pgdata:
2 changes: 1 addition & 1 deletion Dockerfile → pg.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
FROM postgres:13.3
RUN apt update && apt install python3 python3-pip postgresql-plpython3-${PG_MAJOR} -y
RUN echo 'CREATE EXTENSION IF NOT EXISTS plpython3u;' > /docker-entrypoint-initdb.d/py3.sql
COPY ./database.sql /docker-entrypoint-initdb.d/py3.sql
Loading