master
Ernest Litvinenko 2023-11-13 07:38:01 +03:00
commit 7690d9e1b7
28 changed files with 544 additions and 0 deletions

160
.gitignore vendored Normal file
View File

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

0
core/__init__.py Normal file
View File

11
core/handlers/__init__.py Normal file
View File

@ -0,0 +1,11 @@
from fastapi import APIRouter
# Import Routers
from .office import office_router
from .profile import profile_router
router = APIRouter(prefix='/api/v2')
# Including Routers
router.include_router(office_router)
router.include_router(profile_router)

View File

@ -0,0 +1 @@
from .handlers import router as office_router

View File

@ -0,0 +1,18 @@
from fastapi import APIRouter, Depends
from core.models.office.requests import UpdateOfficeRequest
from core.models.office.responses import JdeOfficeDetailResponse
from core.services.office import services
router = APIRouter(prefix='/offices')
@router.get('/offices')
async def list_offices(offices: list[JdeOfficeDetailResponse] = Depends(services.list_offices_service)) -> list[
JdeOfficeDetailResponse]:
return offices
@router.post('/offices')
async def update_office(data: UpdateOfficeRequest):
await services.update_office(data)

View File

@ -0,0 +1 @@
from .handlers import router as profile_router

View File

@ -0,0 +1,30 @@
from fastapi import APIRouter, Path
router = APIRouter(prefix='/profiles')
# todo implement this handlers in services and storages. Then use fastapi.Depends()
@router.post('/')
async def create_profile():
pass
@router.get('/')
async def list_profiles():
pass
@router.get('/{profile_id}')
async def get_profile(profile_id: int = Path()):
pass
@router.delete('/{profile_id}')
async def delete_profile(profile_id: int = Path()):
pass
@router.put('/{profile_id}')
async def update_profile(profile_id: int = Path()):
pass

0
core/models/__init__.py Normal file
View File

View File

38
core/models/office/db.py Normal file
View File

@ -0,0 +1,38 @@
from pydantic import BaseModel
class JdeOfficeDB(BaseModel):
code: str
title: str
kladr_code: str
aex_only: str
mst_pr_aex: str
mst_pr_virt: str
addr: str
features: str
coords: dict[str, str]
city: str
country_code: str
contry_name: str
max_ves: str
max_obyom: str
max_ves_gm: str
max_obyom_gm: str
max_l_gm: str
max_w_gm: str
max_h_gm: str
class JdeOfficeLocalDB(BaseModel):
code: str
features: str | None = None
contact_person_id: int | None = None
person_count: int | None = None
rating: int | None = None
class ProfileDB(BaseModel):
id: int
full_name: str | None
phone: str | None
email: str | None

View File

View File

View File

@ -0,0 +1,16 @@
from pydantic import BaseModel
class UpdateOfficeRequest(BaseModel):
code: int
features: str | None
contact_person_id: int | None
person_count: int | None
rating: int | None
class CreateOrUpdateProfileRequest(BaseModel):
id: int | None
full_name: str
phone: str | None
email: str | None

View File

@ -0,0 +1,17 @@
from pydantic import BaseModel, EmailStr
from .db import ProfileDB, JdeOfficeDB
class ProfileResponse(ProfileDB):
email: EmailStr | None
class ExtendedResponse(BaseModel):
features: str | None
contact_person: ProfileResponse | None
person_count: int | None
rating: int | None
class JdeOfficeDetailResponse(JdeOfficeDB):
changeable_info: ExtendedResponse | None

9
core/registry.py Normal file
View File

@ -0,0 +1,9 @@
from database import Database
from .storages.profile_storage import ProfileStorage
from .storages.office_storage import OfficeStorage
db = Database()
profile_storage = ProfileStorage(db)
office_storage = OfficeStorage(db)

View File

View File

View File

@ -0,0 +1,9 @@
import httpx
from core.models.office.db import JdeOfficeDB
async def grab_data() -> list[JdeOfficeDB]:
async with httpx.AsyncClient() as client:
response = await client.get('http://localhost/api/v1/vD/geo/search?mode=2')
return [JdeOfficeDB.model_validate(x) for x in response.json()]

View File

@ -0,0 +1,19 @@
from .helpers import grab_data
from core.registry import office_storage
from core.models.office.responses import JdeOfficeDetailResponse
from ...models.office.requests import UpdateOfficeRequest
async def list_offices_service() -> list[JdeOfficeDetailResponse]:
offices = await grab_data()
offices.sort(key=lambda x: x.code)
offices_additional_data = {data.code: data for data in office_storage.list_all_offices()}
response_data = [
JdeOfficeDetailResponse(**office.model_dump(), changeable_info=None or offices_additional_data.get(office.code)) for office in offices
]
return response_data
async def update_office(data: UpdateOfficeRequest):
office_storage.update_office(data)

View File

View File

@ -0,0 +1,28 @@
from database import Database
class BaseStorage:
def __init__(self, db: Database):
self.db = db
def __fetch_all(self, sql, *args):
return self.db.fetch_all(sql, *args)
def __fetch_one(self, sql, *args):
return self.db.fetch_one(sql, *args)
def __form_query(self, table_name: str, kwargs) -> str:
sql = f'select * from {table_name}'
if len(kwargs) != 0:
filters = [f"{key}='{val}'" for key, val in kwargs.items()]
sql = f"select * from {table_name} where {' and '.join(filters)}"
return sql
def get_by(self, table_name: str, **kwargs):
return self.__fetch_one(self.__form_query(table_name, kwargs))
def list_by(self, table_name: str, **kwargs):
return self.__fetch_all(self.__form_query(table_name, kwargs))

View File

@ -0,0 +1,34 @@
from fastapi.exceptions import HTTPException
from .base_storage import BaseStorage
from core.models.office.db import JdeOfficeLocalDB
from core.models.office.requests import UpdateOfficeRequest
class OfficeStorage(BaseStorage):
def get_by_id(self, idx: int) -> JdeOfficeLocalDB:
row = self.get_by('office', code=idx)
if row is None:
raise HTTPException(status_code=404, detail='Office not found')
office = JdeOfficeLocalDB(code=row[0], features=row[1], contact_person_id=row[2], person_count=row[3],
rating=row[4])
return office
def list_all_offices(self):
rows = self.list_by('office')
return [
JdeOfficeLocalDB(code=row[0],
features=row[1],
contact_person_id=row[2],
person_count=row[3],
rating=row[4])
for row in rows]
def update_office(self, data: UpdateOfficeRequest):
try:
self.get_by_id(data.code)
sql = f"update office set features = %s, contact_person_id = %s, person_count = %s, rating = %s where code = %s"
self.db.execute(sql, (data.features, data.contact_person_id, data.person_count, data.rating, data.code))
except HTTPException as _:
sql = f"insert into office (code, features, contact_person_id, person_count, rating) values (%s, %s, %s, %s, %s)"
self.db.execute(sql, (data.code, data.features, data.contact_person_id, data.person_count, data.rating))

View File

@ -0,0 +1,30 @@
from fastapi import HTTPException
from .base_storage import BaseStorage
from core.models.office.db import ProfileDB
from core.models.office.requests import CreateOrUpdateProfileRequest
class ProfileStorage(BaseStorage):
def get_by_id(self, id: int):
row = self.get_by('profile', id=id)
if row is None:
raise HTTPException(status_code=404, detail='Profile not found')
return ProfileDB(id=row[0], full_name=row[1], phone=row[2], email=row[3])
def create_or_update_profile(self, data: CreateOrUpdateProfileRequest):
try:
if not data.id:
raise KeyError('No id provided')
self.get_by_id(data.id)
sql = f"update profile set full_name = %s, phone = %s, email = %s where id = %s"
self.db.execute(sql, (data.full_name, data.phone, data.email, data.id))
except (HTTPException, KeyError) as _:
sql = f"insert into profile (full_name, phone, email) values (%s, %s, %s)"
self.db.execute(sql, (data.full_name, data.phone, data.email))
def list_all_profiles(self):
rows = self.list_by('profile')
return [ProfileDB(id=row[0], full_name=row[1], phone=row[2], email=row[3]) for row in rows]

59
database.py Normal file
View File

@ -0,0 +1,59 @@
import typing
import pymysql
class Database:
__instance: 'Database' = None
__client: pymysql.Connection = None
def __new__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = super().__new__(cls)
return cls.__instance
def __init__(self):
self.__create_con()
def __create_con(self):
# TODO Set to environment variables. Use pydantic-settings
self.__client = pymysql.connect(
host='127.0.0.1',
port=3306,
user='root',
password='root',
db='jde',
charset='utf8'
)
def __execute_cmd(self, wrapper, *args, **kwargs):
with self.__client.cursor() as cursor:
data = wrapper(cursor, *args, **kwargs)
self.__client.commit()
return data
@classmethod
def _fetch_all(cls, cursor: pymysql.cursors.Cursor, sql: str, *args) -> list:
#todo research formatting input values in sql queries. Which type of args?
cursor.execute(sql, *args)
return cursor.fetchall()
@classmethod
def _fetch_one(cls, cursor: pymysql.cursors.Cursor, sql: str, *args) -> typing.Any:
#todo research formatting input values in sql queries. Which type of args?
cursor.execute(sql, *args)
return cursor.fetchone()
@classmethod
def _execute(cls, cursor: pymysql.cursors.Cursor, sql: str, *args) -> None:
#todo research formatting input values in sql queries. Which type of args?
cursor.execute(sql, *args)
def fetch_all(self, sql: str, *args) -> list:
return self.__execute_cmd(self._fetch_all, sql, *args)
def fetch_one(self, sql: str, *args) -> typing.Any | None:
return self.__execute_cmd(self._fetch_one, sql, *args)
def execute(self, sql: str, *args) -> None:
return self.__execute_cmd(self._execute, sql, *args)

1
docs/ERD.drawio Normal file
View File

@ -0,0 +1 @@
<mxfile host="drawio-plugin" modified="2023-10-29T21:12:08.379Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36" version="20.5.3" etag="JcmW1FTD10G-E-Wj7tJR" type="embed"><diagram id="bhLovqJHFxc52hLkd4M8" name="Page-1"><mxGraphModel dx="461" dy="366" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0"><root><mxCell id="0"/><mxCell id="1" parent="0"/><mxCell id="2" value="Office" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" vertex="1" parent="1"><mxGeometry x="50" y="60" width="140" height="156" as="geometry"/></mxCell><mxCell id="3" value="code: varchar(64)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="2"><mxGeometry y="26" width="140" height="26" as="geometry"/></mxCell><mxCell id="4" value="features: text" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="2"><mxGeometry y="52" width="140" height="26" as="geometry"/></mxCell><mxCell id="5" value="contact_person: int8 FK" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="2"><mxGeometry y="78" width="140" height="26" as="geometry"/></mxCell><mxCell id="10" value="personal_count: int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="2"><mxGeometry y="104" width="140" height="26" as="geometry"/></mxCell><mxCell id="11" value="rating: int8 &lt;=5 " style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="2"><mxGeometry y="130" width="140" height="26" as="geometry"/></mxCell><mxCell id="12" value="Profile" style="swimlane;fontStyle=0;childLayout=stackLayout;horizontal=1;startSize=26;fillColor=none;horizontalStack=0;resizeParent=1;resizeParentMax=0;resizeLast=0;collapsible=1;marginBottom=0;" vertex="1" parent="1"><mxGeometry x="270" y="60" width="140" height="130" as="geometry"/></mxCell><mxCell id="14" value="id: int8" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="12"><mxGeometry y="26" width="140" height="26" as="geometry"/></mxCell><mxCell id="13" value="full_name: varchar(64)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="12"><mxGeometry y="52" width="140" height="26" as="geometry"/></mxCell><mxCell id="15" value="phone: varchar(30)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="12"><mxGeometry y="78" width="140" height="26" as="geometry"/></mxCell><mxCell id="17" value="email: varchar(64)" style="text;strokeColor=none;fillColor=none;align=left;verticalAlign=top;spacingLeft=4;spacingRight=4;overflow=hidden;rotatable=0;points=[[0,0.5],[1,0.5]];portConstraint=eastwest;" vertex="1" parent="12"><mxGeometry y="104" width="140" height="26" as="geometry"/></mxCell><mxCell id="18" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="14" target="5"><mxGeometry relative="1" as="geometry"/></mxCell></root></mxGraphModel></diagram></mxfile>

23
main.py Normal file
View File

@ -0,0 +1,23 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from core.handlers import router
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=['*']
)
app.include_router(router)
@app.on_event("startup")
async def startup():
print("startup")
if __name__ == '__main__':
uvicorn.run('main:app', host='0.0.0.0', port=8000, reload=True)

21
migrations/initial.sql Normal file
View File

@ -0,0 +1,21 @@
BEGIN;
create database if not exists jde;
use jde;
create table if not exists profile (
id int8 primary key auto_increment,
full_name varchar(64),
phone varchar(30),
email varchar(64)
);
create table if not exists office (
code int8 primary key auto_increment,
features text,
contact_person_id int8 references profile(id),
person_count int8 default 0,
rating int8 default 0
);
COMMIT;

19
requirements.txt Normal file
View File

@ -0,0 +1,19 @@
annotated-types==0.6.0
anyio==3.7.1
certifi==2023.7.22
click==8.1.7
dnspython==2.4.2
email-validator==2.1.0.post1
fastapi==0.104.0
h11==0.14.0
httpcore==0.18.0
httpx==0.25.0
idna==3.4
pydantic==2.4.2
pydantic-to-typescript==1.0.10
pydantic_core==2.10.1
PyMySQL==1.1.0
sniffio==1.3.0
starlette==0.27.0
typing_extensions==4.8.0
uvicorn==0.23.2