From bdd94596c815dea52ea7a7d9eadc50d6befb5fdd Mon Sep 17 00:00:00 2001 From: Dmitry <88444255+DmitryGitHab@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:56:36 +0500 Subject: [PATCH 1/4] start project --- main.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..89659f8 --- /dev/null +++ b/main.py @@ -0,0 +1,21 @@ +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +import routers + +app = FastAPI() + + +app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + +app.include_router(routers.router) + +if __name__ == '__main__': + uvicorn.run("main:app", host='127.0.0.1', port=8080, reload=True) \ No newline at end of file From d40145266ae25ca97bc3e8e0f664a16762760600 Mon Sep 17 00:00:00 2001 From: Dmitry <88444255+DmitryGitHab@users.noreply.github.com> Date: Sat, 28 Oct 2023 22:33:12 +0500 Subject: [PATCH 2/4] Add files via upload --- models/__init__.py | 0 models/activity.py | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 models/__init__.py create mode 100644 models/activity.py diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/activity.py b/models/activity.py new file mode 100644 index 0000000..6661a2f --- /dev/null +++ b/models/activity.py @@ -0,0 +1,27 @@ +from datetime import datetime +from enum import Enum +from uuid import UUID, uuid4 + +from pydantic import BaseModel, Field + + +class ServiceStatus(str, Enum): + ACTIVE = 'ACTIVE' + UNSTABLE = 'UNSTABLE' + STOPPED = 'STOPPED' + + + +class CurrentActivity(BaseModel): + service_name: str = Field(example='Service_name') + status: ServiceStatus = Field(examle=ServiceStatus.ACTIVE) + description: str = Field(example='stable operation of the server') + + +class ServicesList(CurrentActivity): + status_map: dict + + +class ServicesLog(CurrentActivity): + log_uuid: UUID = Field(default_factory=uuid4) + created_date: datetime From 49e3a296881cf54a485347c422fc4a21bfddc320 Mon Sep 17 00:00:00 2001 From: Dmitry <88444255+DmitryGitHab@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:34:14 +0500 Subject: [PATCH 3/4] Add files via upload --- __pycache__/database.cpython-310.pyc | Bin 0 -> 2614 bytes __pycache__/main.cpython-310.pyc | Bin 0 -> 887 bytes __pycache__/settings.cpython-310.pyc | Bin 0 -> 689 bytes database.py | 58 +++++++++++++++++ main.py | 55 ++++++++++------ models/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 162 bytes models/__pycache__/activity.cpython-310.pyc | Bin 0 -> 1330 bytes models/activity.py | 4 +- readme.md | 27 ++++++++ requirements.txt | 35 ++++++++++ routers/__init__.py | 7 ++ routers/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 292 bytes .../__pycache__/log_router.cpython-310.pyc | Bin 0 -> 1174 bytes routers/log_router.py | 33 ++++++++++ schemas/__init__.py | 2 + schemas/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 265 bytes schemas/__pycache__/base.cpython-310.pyc | Bin 0 -> 1182 bytes .../services_logs_schemas.cpython-310.pyc | Bin 0 -> 887 bytes .../services_schemas.cpython-310.pyc | Bin 0 -> 743 bytes schemas/base.py | 23 +++++++ schemas/services_logs_schemas.py | 15 +++++ schemas/services_schemas.py | 12 ++++ services/__init__.py | 0 services/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 142 bytes .../__pycache__/log_service.cpython-310.pyc | Bin 0 -> 2532 bytes .../service_service.cpython-310.pyc | Bin 0 -> 1430 bytes services/log_service.py | 60 ++++++++++++++++++ services/service_service.py | 24 +++++++ settings.py | 17 +++++ 29 files changed, 349 insertions(+), 23 deletions(-) create mode 100644 __pycache__/database.cpython-310.pyc create mode 100644 __pycache__/main.cpython-310.pyc create mode 100644 __pycache__/settings.cpython-310.pyc create mode 100644 database.py create mode 100644 models/__pycache__/__init__.cpython-310.pyc create mode 100644 models/__pycache__/activity.cpython-310.pyc create mode 100644 readme.md create mode 100644 requirements.txt create mode 100644 routers/__init__.py create mode 100644 routers/__pycache__/__init__.cpython-310.pyc create mode 100644 routers/__pycache__/log_router.cpython-310.pyc create mode 100644 routers/log_router.py create mode 100644 schemas/__init__.py create mode 100644 schemas/__pycache__/__init__.cpython-310.pyc create mode 100644 schemas/__pycache__/base.cpython-310.pyc create mode 100644 schemas/__pycache__/services_logs_schemas.cpython-310.pyc create mode 100644 schemas/__pycache__/services_schemas.cpython-310.pyc create mode 100644 schemas/base.py create mode 100644 schemas/services_logs_schemas.py create mode 100644 schemas/services_schemas.py create mode 100644 services/__init__.py create mode 100644 services/__pycache__/__init__.cpython-310.pyc create mode 100644 services/__pycache__/log_service.cpython-310.pyc create mode 100644 services/__pycache__/service_service.cpython-310.pyc create mode 100644 services/log_service.py create mode 100644 services/service_service.py create mode 100644 settings.py diff --git a/__pycache__/database.cpython-310.pyc b/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..776094fb4f0ddb9abc4205f11e9768cf0022aa36 GIT binary patch literal 2614 zcmaJ@&2Jk;6rb5$d)JQbxNc~piXx~4$gOZcC?G{u)HZD?QV4Agin%fA9C+Y*MY330!JF z4M+Y(DazaslIN?sn*4-Sj0oI+82Zi8tg*lL`w$ z8C>rsQf&w>H^U2h)$Yc^#fu~3!brIuCV?M52H%BGu6}&w(Bbl>J6}VT77rdLX|%tyQ!>#jY-3|Zr6BguawsfG~_-hl#ugR!&^Xh-)y)j9ucI0Aqm_ zcOj9W*~|g0z)HNxOlCc@5Pq=OnT0#dx=FxVVO6k}m<6!Uro|1eJhmRtu}x9OdkJod zU@+1q`*dZW08*CZ;Uh5%RxREv-db4WtQY8Y6(Yp=as@T6l$u^YGz+QzJ2KsXY=Q3@JZV^#~K$>=1%s7@7);W+dbwn@rketGS z`P7NH@)3r#JYsGb3Vi#0Q`FJJ3X(WZ3mj|U2ySr#=SDOJ6hkgggXwhxgz>_l4SI@R zIj9*m>bx)x26H)(`6k%|{|TbN9ON$`-a`Ep3fw@4E#^T>9VU{l=jZW?GdO$5hb+Ju z?UL>G4$h+(H1c4^CV@^WQUSjB=t9DcvMPUr0c3Sqk)5NQezt84jma!i0v2OP_aJMa znR|I2r)4^xIn46K<5~V+PTV=}tvO-!zXMK~W1KLbn>i;yJK{tkt!C{hZ%P{6)btsH z>epRB6QF5n0*LqE)l+-q`!jJQ{)26Uhb$i`pu2S%Y`%)OaEn8%(}%Gp?( z47+KFld!-_vy5j!>_^=5(z5483G0QZS3GaC=ZD#hz(yy|qIeetc2n^Ih}6QD)~Z-4pF`{u!N m)`E5FF#2h)l{(iloE9V+;p*e3sB;pwrN$q`< zo;7g2dAA1Fj?>AOS zHv7~iZ@ICf?F|gOueWx_?{gs}9B~bpiDzJ$*uy9_U~OK`jH8ct`;qH#Db*2EI-ljG zalK18)gYj>IhW>oM*((Ui5s{NT!1z%F_vq)sHVP->p?LH(0-zs0lF{Ll2h`DSUL}M z_aiwEFUSTe`HoP6D7r<0j7gtv_8U4vo^GIXo%NlqZMHtz9laWj-?$~ARmCjdlYr%5 zsocU9^bHD*OWeQ@qF0dpXShC`fLKhq%~d(sKAu!s&0%WI#4E;ll`{kS5Hz!Z*m83) zDR^EEs$W;x7%JBeyXZ-64KF2{!yL6T6RbODrm}f9{lKOebtLmh=_Vn4{34Uw& zw}*uELl^BUV_j4!ZC$KEDlYmxmn;tRsnVrOK43XmjrPj9`&VdC+}z^;w;7FZmbgT# zg+=-GGNL<5g?O^-av|6)&2{r~Di;FSB?-^2$u3ppWnVWS~o2p&7&{j>CgB8gNv3CiiNwD3O+ACan z4>Y_+!h*KcNQkL__i8U+a2&5u(2!U+Ag#@*(EaRx&@2H}X~5^XTW z7`;K9aOWH1j=*0XN(A99@A3Y3LLKfUDDrP$1&Xi+z0mPpk*I;K!cWpmWM45fkQaT$oRMU&&_DM zS}fT#UdNL-nVCUZ=UMr&R@!*e346Op*3^CZ;`y`ITrO7Yeb7MkN(%L;bI#*;GvnVn z+j$qsY}GA@lVl!+#%HX`Hi9t|Ft(}rwzM2F_GO!uT}0ZqHSUMHF3o+$vZ|_eri;2# zjLE)@IZ8|E0ZHGAt&|*s{6zBDvXwul)7Pos+q_+xE_Z1o>rW!rDm~AXW?54(CFHe` zObf7eraq_Ye>KL None: + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + except OperationalError as error: + pass + + async def drop_tables(self) -> None: + try: + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + except OperationalError as error: + pass + + async def insert(self, stmt: Base) -> None: + try: + async with async_session() as session: + async with session.begin(): + session.add(stmt) + await session.commit() + except OperationalError as error: + pass + + async def execute(self, stmt: Base) -> list | None: + try: + async with async_session() as session: + async with session.begin(): + result = await session.execute(stmt) + try: + result = [u._asdict() for u in result.all()] + except ResourceClosedError: + return None + return result if len(result) > 0 else None + + except OperationalError as error: + pass diff --git a/main.py b/main.py index 89659f8..e6c5328 100644 --- a/main.py +++ b/main.py @@ -1,21 +1,34 @@ -import uvicorn -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware - -import routers - -app = FastAPI() - - -app.add_middleware( - CORSMiddleware, - allow_origins=['*'], - allow_credentials=True, - allow_methods=['*'], - allow_headers=['*'], -) - -app.include_router(routers.router) - -if __name__ == '__main__': - uvicorn.run("main:app", host='127.0.0.1', port=8080, reload=True) \ No newline at end of file +import uvicorn +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +import routers +from database import get_db +from settings import settings + + +app = FastAPI() + + +app.add_middleware( + CORSMiddleware, + allow_origins=['*'], + allow_credentials=True, + allow_methods=['*'], + allow_headers=['*'], +) + +app.include_router(routers.router) + + +@app.on_event('startup') +async def startup_event(): + database = get_db() + if settings.DROP_DATABASE: + await database.drop_tables() + await database.create_tables() + + +if __name__ == '__main__': + + uvicorn.run("main:app", host='127.0.0.1', port=8000, reload=True) \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-310.pyc b/models/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eccc87e75a6fcc9eed223d4c2a8675dc4e98ca08 GIT binary patch literal 162 zcmd1j<>g`k0`a7YDIoeWh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6w&#VRH>r8GIQ zBr`uRCZIB=peR2pHMyiX#x1e9BtEeqGrl;rs4TT8z9bdMFG(!Uj>*kWNzEyaiI30B c%PfhH*DI*J#bE;!EX_%^0~ue;1SD7(03|ml7XSbN literal 0 HcmV?d00001 diff --git a/models/__pycache__/activity.cpython-310.pyc b/models/__pycache__/activity.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..89220506b3b57056737bb09c3887afc3f86b7bff GIT binary patch literal 1330 zcmZ`(&2HQ_5GEz+XSG^;lcxDWQNYLUC8wMUqp%aNg9Ogjz}h_!3lM@h4wOi%ElCC4 z9(qZyd5}CpUxnA6@(Mj=M%o01>j=oz9-UKq}?XV6TAw?6(sP{!3H;GJ|RHmFgXFAgH-%Q6ag5bT78AcMF zVkCv+eJIE1k93B924nPd^v60!KkxmCo-EmH?-S<0X56Q`aNw#2&=&J`*USPhE-zo4 z_^9hD{RG|7)55~5RzvNh(+X-`(%S1EN9PEC`w#(`pU zRS8R1xX$|c_}Rsa-{wBOTr4k+pPtQqvb^~9{Cs}0n8iL-YF#v-l+ToETHV!T=SuzF z74(%*owK44*D4kuM7S^d^Qz?ttfKd+6 zcZGes!c(BOtDWQ3<{Qk4f7+AaGT~adNk=V0?swMD8(QIn) zIn{&gejU(oRdluUvF-W(^A4>9b5PlXem6MqV-2=6)rLq~%*H0i)Xjuok02rV4#9`k z8NaVov0k^h8&0=M^*jtBd`~m)6MU6mu%;BdgFx|~#f;tM_xOF@N59P?ZFZeS5Sk+^ z0kRVMFC^Hr$~u`11*)dlEN0P-BD~O5=?q=XTXTDa1C+fL2pP+{p~$!M`UyvG=7mb4 zwvgxmmBc2uXvg9xwwUc1TI2;_#~Zr1UQiChrgVl+H(On-T~!)d rJRJUjENX;ax}K+gzuS2A>EY0{ztHPo=^Ern?vzi1yWupM#`nZ$0_+*w literal 0 HcmV?d00001 diff --git a/models/activity.py b/models/activity.py index 6661a2f..54db732 100644 --- a/models/activity.py +++ b/models/activity.py @@ -14,8 +14,8 @@ class ServiceStatus(str, Enum): class CurrentActivity(BaseModel): service_name: str = Field(example='Service_name') - status: ServiceStatus = Field(examle=ServiceStatus.ACTIVE) - description: str = Field(example='stable operation of the server') + status: ServiceStatus = Field(default=ServiceStatus.ACTIVE) + description: str = Field(example='Server is Active ') class ServicesList(CurrentActivity): diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..30f9878 --- /dev/null +++ b/readme.md @@ -0,0 +1,27 @@ +# Python TEST TASK for itpc.ru +Этот проект представляет собой API, которое позволяет выполнять следующие действия: + + - Сохранение данных: API принимает данные, включая имя сервиса, его текущее состояние и описание, и сохраняет их в базе данных. + - Вывод списка сервисов с актуальным состоянием: API предоставляет эндпоинт для получения списка сервисов с их текущим состоянием. + - История изменения состояния: По имени сервиса API позволяет получать историю изменения состояния и всю доступную информацию по каждому состоянию сервиса. + +## Ресурсы +- [FastAPI](https://fastapi.tiangolo.com/) +- [SQLalchemy](https://www.sqlalchemy.org/) +- [Pydantic](https://docs.pydantic.dev/latest/) +- [PostgreSQL](https://www.postgresql.org/) + + +## Документация +Установка зависимостей: +``` +pip install -r reuirements.txt +``` +Запуск через main.py, либо через консоль: +``` +uvicorn main:app --reload +``` + + +После запуска документация доступна по адресу http://127.0.0.1:8000/docs/ +Реализовано через OpenAPI(Swagger) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c8f0d5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,35 @@ +annotated-types==0.6.0 +anyio==3.7.1 +asyncpg==0.28.0 +certifi==2023.7.22 +click==8.1.7 +colorama==0.4.6 +dnspython==2.4.2 +email-validator==2.1.0.post1 +exceptiongroup==1.1.3 +fastapi==0.104.0 +greenlet==3.0.1 +h11==0.14.0 +httpcore==0.18.0 +httptools==0.6.1 +httpx==0.25.0 +idna==3.4 +itsdangerous==2.1.2 +Jinja2==3.1.2 +MarkupSafe==2.1.3 +orjson==3.9.10 +pydantic==2.4.2 +pydantic-extra-types==2.1.0 +pydantic-settings==2.0.3 +pydantic_core==2.10.1 +python-dotenv==1.0.0 +python-multipart==0.0.6 +PyYAML==6.0.1 +sniffio==1.3.0 +SQLAlchemy==2.0.22 +starlette==0.27.0 +typing_extensions==4.8.0 +ujson==5.8.0 +uvicorn==0.23.2 +watchfiles==0.21.0 +websockets==12.0 diff --git a/routers/__init__.py b/routers/__init__.py new file mode 100644 index 0000000..cea3fce --- /dev/null +++ b/routers/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from routers import log_router + +router = APIRouter() + +router.include_router(log_router.router) diff --git a/routers/__pycache__/__init__.cpython-310.pyc b/routers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9b785e436ca65eadac0ffd960ff2a3aecab1e6b GIT binary patch literal 292 zcmYj~%}N6?5XbYe-PY2Iud$atKoPM}3cX0t+Y$)NW|Y+IW=XPzUcC4s_UHrnGP#K- zU%^u+Z4VC2$B+Naf9!NRCK%seb9lr4li+`FgnLXs!=i|yhBUOKG|kMghL@ZYpzoy2 zAo~~q-)4uT@+VSczj#9m8m|_c^`q?rIE1mWFVdapp~&O#x$>dvv}3p%hiAdN^f%^b zir2cWO|Rf`_DS3b^z*6mlRdi=Ximc2eGT5;-GpT6R1*mNM z5ACvlz={q3z+$$nShIpf%e{_W3KApD^?jVVk8_i}+iep#pFXGZr$fj$H0~c4jAwB5 zR}g{-nv#r0l%md3mN}7=@rakUlQuFpax*XTvS!pQeLQVt?WkSqjkJ^bkxz+qge$xm z70nA4bz4MsFMFbOMI}EcQ!3ih6&=~0a^Z{a$HqC00>r!tMNd-cJs=`@PDFUYD*TG{ zr%bGhwGz8hVf%n>0`?DXU9MJnH^gR%T@%3(8Ek!r*~oxeevoK0V79e$xc@p|7^$qc zD+@Undf?dL#X>1LH#-xP94F?)`bSb7CljghneE(J`3!Qk5As>%0V}A3)%Ub9O!JwJ zIG{9CbPFY?$vewUJkzJ0p^lH`7!gCuMQmal##Pm^UKX7&Y`QwA!}iD*xIb1we4uCa zlAMt%x(}P$BS++#zJfk{$T_Z44Vdx)tGXbriCTfruf5&JBPkY>*d+OUba*lM0ag1ryj(sLRNQ(&u}LJzU4 zo|3xA=UR@lT*%Z0)r>W2LLo(cP{(<3R*gkqR&q(MNIj_vzzJEqe-#=~)dzn$g`k0_CYzscAs^F^GcZpO)Sn1Z7GtkmR^;+VvO pf*7c_nE3e2yv&mLc)fzkTO2mI`6;D2sdkJ&LyB2|1P_A%BLJLSM%4fS literal 0 HcmV?d00001 diff --git a/schemas/__pycache__/base.cpython-310.pyc b/schemas/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f53e29a385d422430726f644da4e264850416ba GIT binary patch literal 1182 zcmZWo%TC)s6rHgh$8jFeLf>7aRTs5XMO{}BqCksSfRK`ItW;Cu8K|kp4l@pI5UgO! zfX(P@;CE8Q)j#BLF7|S9l7&AU&GUuZd?A9c4S5vsP81StY+Wc-L202F z4+V>|EH41nJYxW)s$n1^W$Y~fs^(GkP@Q8lp_(yu7p&JrJ;4Zke!W_IGZ1_fm&pd( z2Uc%7F#e-}N~`nQ*k8o>76paf8NZt*x`=Z*T)L0R zu=hoB^ti~GK1e&=G>@ZHFM)6q2FZZO-9)4uz%>pe)d6hPFqsU%rUR&Fkm}!Z?j-L5 zmO}1)t3@5Eo`bj*v`X@g4~3%?Nf=L70w*mOVM2H7*5ApMR8@2%LFG!AdoykvVbYGO jV*pZF7Mtm-T#FJ5uRGb!B_vd1Y=bfjN5ViBY*<{-TMO5Oz4ee!5oT`GVYE^`Q3aQ0U^Jgv%4a6PH>w`G=c~!iK2{B zhXWa?kcA$HGEy;%Rl*YQM>16z%RG+dNaZZ2jsk6@vuC1?Sg8C0$YUO3r&%bL(n1M;$tY&x7c~8TTmc9_LllwD?&p zZaCM8ix#^r>WK@5rq_BO(NChE;Ni{Rm-C|~h@tk^mmfCEPPd<-?#&W^_R`d!KviaW zuiTG~-$iHLMrT;ayQJ zO0JNocm*oP$+D~?PiDRu&wMj>yIAA|*UxVk@SPI!%NwUFLgN{3yG9|2q>5GZSh-4tcJIb&iq>l(>GFp=njS-z4(d2-p zh~`H$^C-+~B4>92D`bvz=WezG^SQ3za>Ft^srEIpn|G$x!tD7c*t_E3;f>JXo2wfM zUtD5)(GM1ZtQnLN*9zQRf~`#5^>yo;OeMS%gX*`-h0FMOo^zLTu3I@MK)vAnW)R9} zMr*-&@AFKi@zXGw7ac4XLR&zmzjHP zZZ@yf5+=qfW9I+lC+!>^E?ZBd Sdx{U4SU;PBoY5k95dH;JUBKx8 literal 0 HcmV?d00001 diff --git a/schemas/base.py b/schemas/base.py new file mode 100644 index 0000000..b91e6ed --- /dev/null +++ b/schemas/base.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from sqlalchemy import BIGINT, TIMESTAMP, String, func +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy.sql.schema import Column + + +class Base(DeclarativeBase): + type_annotation_map = { + int: BIGINT, + datetime: TIMESTAMP(timezone=False), + str: String(), + } + + created_date: Mapped[datetime] = mapped_column(server_default=func.now(), index=True) + update_date: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now()) + + +def to_sql(pidantic_schemas): + """Convert Pydantic schemas to column names""" + keys = pidantic_schemas.__fields__.keys() + keys = [Column(key) for key in keys] + return keys diff --git a/schemas/services_logs_schemas.py b/schemas/services_logs_schemas.py new file mode 100644 index 0000000..fc09588 --- /dev/null +++ b/schemas/services_logs_schemas.py @@ -0,0 +1,15 @@ +from uuid import uuid4 + +from sqlalchemy import UUID +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.sql.schema import ForeignKey + +from schemas.base import Base + + +class ServiceLogDB(Base): + __tablename__ = 'services_logs' + log_uuid: Mapped[UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid4) + service_name = mapped_column(ForeignKey('services.service_name'), nullable=True) + status: Mapped[str] = mapped_column(nullable=False) + description: Mapped[str] = mapped_column(nullable=False, default='') diff --git a/schemas/services_schemas.py b/schemas/services_schemas.py new file mode 100644 index 0000000..b4f50bd --- /dev/null +++ b/schemas/services_schemas.py @@ -0,0 +1,12 @@ +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column + +from schemas.base import Base + + +class ServiceDB(Base): + __tablename__ = 'services' + service_name: Mapped[str] = mapped_column(primary_key=True) + status: Mapped[str] = mapped_column(index=True) + description: Mapped[str] = mapped_column(nullable=True) + status_map = mapped_column(JSONB, default={}) diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/__pycache__/__init__.cpython-310.pyc b/services/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8d25121109b025307a453aef74542d3b82c5fa4 GIT binary patch literal 142 zcmd1j<>g`k0`a7YDIoeWh(HF6K#l_t7qb9~6oz01O-8?!3`HPe1o6wz#VRH>r8GIQ zBr`uRCZIB=peR2pHMyiXCb6I(rZ}~zEHgQ^I3_+mGcU6wK3=b&@)n0pZhlH>PO2Tq LsA47{!NLFl^vEC9 literal 0 HcmV?d00001 diff --git a/services/__pycache__/log_service.cpython-310.pyc b/services/__pycache__/log_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a02a2e825b2e4bed50ab940bfa955634b45521cb GIT binary patch literal 2532 zcmZ`*&2HO95Z+x<6iLxPk)6a2k^pV`6J3Pf3KT|=G(nmMfl<^wEgTRucdbw%MR|9X z)CLWta85l0=o`pDFMW=_1+P8j4T_>bIxKp^?wS6b=6dw1i?&jUX=f2fDc~FErEPA|G zL_D%%UcOW;^JS}d^Oa(iuUg&DFBN^>w|bDT73+MRl5dCzMej8cy%WYSi)e=omfnIr zWI$_wyZm07nVJk(?P;0I)YRT=DiR}M*2^YZDudccnpo@&oOVWH!B@Ld%+kcbmfi0Tcc-cx1MqtHVWLf(Of$Pvs@O>An?yg~Jv&P8=H;k8cQ`$) z{W#7hnTg{SI9Ve~K8@(n+WU(Q+ZA=x+BHcEnd1H02Wg5I`5j*U7{pJsB4aX#`x@tj z&Rye-I~9G-PU+kOy*u|$3Fv{Kr||n0==_Krvx=!(6%%Ze2xk)zvM9ORZxK#qMGv$= zsL`5rzf=Ms*aL#4N&+86jI69(A$3}J@~p_rz)_cQ zGj6E;9MJ=OEXs{o!-)vF+fL$$UIoEkdDH`}B9P(V>)z4T1Su2ffHd*en-hxXHTugNCyzA6jrUA2L8 z=&B2D19bH)Hy>IUoPYm1?9dSi_R0reU8mu@E6&mSf0+v8NKA6UQ3%61wf}?;ItrkDrX#RkogJ_#>W-VUp=Mz2SFHBZVfY+aPKOPOO(qX_6ccPRohVP=~A?3bUC)$6MwMf+p5Kg8_b?%B&H{ z5Wbo=bqP6ZGX>vR99hgE_S5Qf+!YF$o5bG#Lk328P;yq~Zoy)W7PD6o>$5)f7!omZ zFon3xrOVKUv`-`Ev72=H-Gy|rc+&QuhoE2%j^jFv=TIXt$tk{6;&}Rydn;;833l5EVb_0U<4iJODzd(25@kMM%A^BCKWYwwrpB*7g=!1*xb# zbK_nlE<6Ws;VUOzffF;{1kzaY?07t5&-cxEC%s;m;CS^+eDw+W>nPATf1^lCr%o$kw^4)f6IfsE=nkLx5)+&YwK@E(m^uYv$OV}a23PEoA9Rp;3U9Zu7Ft)6F(euQ&b=Q*^}zmT zu=98*cvBVzHivIdhev8M0v7b}MWGEVjw+@Bu~5tiSers04$*)V%oXPWhW^p1O&F`j zm0@fJ6T-|=dUksWL4WRK174WIMoO4QjZ1W&4{U}A=1-Vfg7{3QWJIp0@r-{#uL9$b z!YLhvhu$~(jlPB^c||^XQ%~KQ0=FB`G8;g_EGEL3QOx~{Kuq$TexX+#<6n>bAC(0I zh0Ll11rn8Y6(X{+_$bQ82!;8%Z5w_#5K7o|MhhDkwNMDfMkj@Agho<$RRSrt(;}_L z`TxuO1|HUF5?!H#6;^sPbXR+f#-2}bmAw~_WWmnoq#P-yrcJx34B2KKaZlqZT z*OTy$=IHb-@YHyYcfJY8&h9N^y5>E|5~LRS25P&E)f3)G)KkWen?klbY7Gul2(~Tf z25NUvU||;o@~S!@5TwS^CnTXa{WNjbLAGk|!&@(8c_8Z3KCcS=U+DhPM4NqpcPw4m z^BW6Jdku7U+buT1;=M1Rl list: + service_name = service_name.lower() # ToDO : проверки нужны по шаблонам и тд, тк дыра в безопасности + result = await self.session.execute( + select(*to_sql(ServicesLog)).select_from(ServiceLogDB).where(ServiceLogDB.service_name == service_name) + ) + return [ServicesLog(**_) for _ in result] if result else [] + + async def log_activity(self, data: CurrentActivity) -> NoReturn: + + data.service_name = data.service_name.lower() # ToDO : проверки нужны по шаблонам и тд, тк дыра в безопасности + + service_data = await self.service_service.get_service_data(data) + last_log = await self.get_last_log(data.service_name) + + current_log_date = await self.session.execute( + insert(ServiceLogDB).values(**data.__dict__).returning(ServiceLogDB.created_date) + ) + current_log_created_date = current_log_date[0]['created_date'] + status_map = service_data.status_map + all_time = status_map.get('time_all', 0) + + delta_time = 0 + if last_log: + delta_time = (current_log_created_date - last_log.created_date).total_seconds() + if not status_map.get(last_log.status): + status_map[last_log.status] = 0 + status_map[last_log.status] += delta_time + + status_map['all_time'] = all_time + delta_time + + await self.session.execute( + update(ServiceDB).values({'status_map': status_map}).where(ServiceDB.service_name == data.service_name) + ) \ No newline at end of file diff --git a/services/service_service.py b/services/service_service.py new file mode 100644 index 0000000..a529f1e --- /dev/null +++ b/services/service_service.py @@ -0,0 +1,24 @@ +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.future import select + +from database import get_db +from models.activity import CurrentActivity, ServicesList +from schemas.base import to_sql +from schemas.services_schemas import ServiceDB + + +class ServicesService: + def __init__(self): + self.session = get_db() + + async def get_service_data(self, data: CurrentActivity) -> ServiceDB: + service_data = await self.session.execute(select(ServiceDB).where(ServiceDB.service_name == data.service_name)) + if service_data: + return service_data[0]['ServiceDB'] + + service_data = await self.session.execute(insert(ServiceDB).values(**data.__dict__).returning(ServiceDB)) + return service_data[0]['ServiceDB'] + + async def list_services(self): + result = await self.session.execute(select(*to_sql(ServicesList)).select_from(ServiceDB)) + return result diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..e0547cd --- /dev/null +++ b/settings.py @@ -0,0 +1,17 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + # SQLALCHEMY_URL: str = 'postgresql+asyncpg://postgres:postgres@localhost:5432/postgres 2' + DROP_DATABASE: bool = False + + DB_HOST: str = 'localhost' + DB_PORT: str = '5432' + DB_NAME: str = 'postgres 2' + DB_USER: str = 'postgres' + DB_PASS: str = 'postgres' + +settings = Settings( + _env_file='../.env', + _env_file_encoding='utf-8', +) From b28bd0f8cc0218c0f73c49f3fb5263ef7a03c9e9 Mon Sep 17 00:00:00 2001 From: Dmitry <88444255+DmitryGitHab@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:36:20 +0500 Subject: [PATCH 4/4] Add files via upload --- README.md | 91 +++++++++++++++++-------------------------------------- 1 file changed, 27 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index cfb476f..30f9878 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,27 @@ -# Вакансия :: Программист Python - -Разработка бизнес-системы с использованием веб-технологий. Автоматизация сервисов с большим количеством пользователей - -## От вас - -### Обязательно - -- Знание синтаксиса языка Python -- Опыт разработки на Python не менее 1 года -- Базовые знания принципов работы Web -- Желание работать в команде и развиваться - -### Приветствуется - -- Навыки работы с Flask, Sanic, FastAPI -- Опыт работы с БД: PostgreSQL, MS SQL, MongoDB, ClickHouse -- Опыт разработки под ОС семейства GNU Linux, знание основных команд -- Работа с системами управления исходным кодом Git -- Знания базовых принципов разработки (тестирование, рефакторинг, Code Review, CI/CD) - -### Будет круто, но не обязательно - -- Знание английского языка на уровне чтения технической документации -- Участие в разработке Open Source проектов -- Наличие профиля на GitHub, Stack Overflow -- Наличие проектов которые можете показать нам -- Разработка с использованием TypeScript, знание современных frontend-библиотек и подходов к разработке - -## У нас - -- Полный рабочий день, гибкий обед и начало рабочего дня -- Полностью «белая» заработная плата с возможностью увеличения в процессе работы (зависит от отдачи сотрудника) -- Полис ДМС -- Дружелюбная команда с юмором, готовая поддержать и помочь -- Интересный проект и необычные задачи. Рутина тоже есть, но мы нацелены именно на продуктив -- Возможность одновременно участвовать в разных проектах и развивать другие компетенции (TS и все модное) -- Попробовать современные тренды и практики в разработке ПО -- Никаких опенспейсов и кубиклов, а комфортное пространство в центре Тюмени -- Готовы делиться опытом и знаниями, если вы готовы их получать - -  - -Если вакансия вас заинтересовала, но есть недопонимания и вопросы, свяжитесь с нами - обсудим, договоримся. -Большим плюсом будет выполнение тестового задания. -Если у вас есть опыт работы с 1С, то эта вакансия не для вас. - -## Тестовое задание - -Решение принимается в виде PR к текущему проекту. - -Есть несколько рабочих сервисов, у каждого сервиса есть состояние работает/не работает/работает нестабильно. - -Требуется написать API который: - -1. Получает и сохраняет данные: имя, состояние, описание -2. Выводит список сервисов с актуальным состоянием -3. По имени сервиса выдает историю изменения состояния и все данные по каждому состоянию - -Дополнительным плюсом будет - -1. По указанному интервалу выдается информация о том сколько не работал сервис и считать SLA в процентах до 3-й запятой - -Вывод всех данных должен быть в формате JSON +# Python TEST TASK for itpc.ru +Этот проект представляет собой API, которое позволяет выполнять следующие действия: + + - Сохранение данных: API принимает данные, включая имя сервиса, его текущее состояние и описание, и сохраняет их в базе данных. + - Вывод списка сервисов с актуальным состоянием: API предоставляет эндпоинт для получения списка сервисов с их текущим состоянием. + - История изменения состояния: По имени сервиса API позволяет получать историю изменения состояния и всю доступную информацию по каждому состоянию сервиса. + +## Ресурсы +- [FastAPI](https://fastapi.tiangolo.com/) +- [SQLalchemy](https://www.sqlalchemy.org/) +- [Pydantic](https://docs.pydantic.dev/latest/) +- [PostgreSQL](https://www.postgresql.org/) + + +## Документация +Установка зависимостей: +``` +pip install -r reuirements.txt +``` +Запуск через main.py, либо через консоль: +``` +uvicorn main:app --reload +``` + + +После запуска документация доступна по адресу http://127.0.0.1:8000/docs/ +Реализовано через OpenAPI(Swagger)