From 7b52ad9baca63a7e4d47b71d945777f77e4df038 Mon Sep 17 00:00:00 2001 From: abubakar Date: Tue, 7 Oct 2025 16:18:33 +0500 Subject: [PATCH 01/10] implemented auth functionality --- auth.py | 56 ++++++++++ database_model.py | 10 +- main.py | 103 +++--------------- model.py | 33 +++++- routes/__init__.py | 0 routes/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 162 bytes .../products_routes.cpython-313.pyc | Bin 0 -> 4980 bytes routes/__pycache__/user_route.cpython-313.pyc | Bin 0 -> 3602 bytes routes/products_routes.py | 94 ++++++++++++++++ routes/user_route.py | 60 ++++++++++ 10 files changed, 260 insertions(+), 96 deletions(-) create mode 100644 auth.py create mode 100644 routes/__init__.py create mode 100644 routes/__pycache__/__init__.cpython-313.pyc create mode 100644 routes/__pycache__/products_routes.cpython-313.pyc create mode 100644 routes/__pycache__/user_route.cpython-313.pyc create mode 100644 routes/products_routes.py create mode 100644 routes/user_route.py diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..45a21e2 --- /dev/null +++ b/auth.py @@ -0,0 +1,56 @@ +from datetime import datetime, timedelta +from typing import Annotated +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from database import SessionLocal +import database_model +from passlib.context import CryptContext +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from jose import JWTError, jwt + + +SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +oauth2_bearer = OAuth2PasswordBearer(tokenUrl="/api/v1/users/token") + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# create token here + + +def create_access_token(username: str, user_id: int, expires_delta: timedelta = None): + encode = {"sub": username, "id": user_id} + expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) + encode.update({"exp": expire}) + return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM) + +# verify token here + + +def get_current_user(token: str = Depends(oauth2_bearer), db: Session = Depends(get_db)): + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + user_id: int = payload.get("id") + if username is None or user_id is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalidate credentials") + user = db.query(database_model.User).filter( + database_model.User.id == user_id).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found" + ) + return user + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token") diff --git a/database_model.py b/database_model.py index ef72229..30102fe 100644 --- a/database_model.py +++ b/database_model.py @@ -11,4 +11,12 @@ class Product(Base): name = Column(String) description= Column(String) price=Column(Float) - quantity=Column(Integer) \ No newline at end of file + quantity=Column(Integer) + + +class User(Base): + __tablename__ = "users" + + id= Column(Integer,primary_key=True,index=True) + username=Column(String,unique=True,index=True) + password=Column(String) \ No newline at end of file diff --git a/main.py b/main.py index 412cd1c..98e85c3 100644 --- a/main.py +++ b/main.py @@ -1,11 +1,13 @@ -from fastapi import Depends, FastAPI +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from model import Product -from database import SessionLocal, engine import database_model -from sqlalchemy.orm import Session +from database import engine +from routes import products_routes, user_route -app = FastAPI() +version = "v1" +app = FastAPI(title="Fastapi ", + description="this is learning project.", + version=version,) app.add_middleware( CORSMiddleware, @@ -15,92 +17,13 @@ database_model.Base.metadata.create_all(bind=engine) - -products = [ - Product(id=1, name="Laptop", description="A high-performance laptop", - price=999.99, quantity=10), - Product(id=2, name="Smartphone", - description="A latest model smartphone", price=699.99, quantity=25), - Product(id=3, name="Headphones", - description="Noise-cancelling headphones", price=199.99, quantity=15), - Product(id=4, name="Monitor", description="4K UHD Monitor", - price=299.99, quantity=8), - Product(id=5, name="Keyboard", description="Mechanical keyboard", - price=89.99, quantity=30), -] - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -def init_db(): - db = SessionLocal() - count = db.query(database_model.Product).count() - - if count == 0: - for product in products: - db.add(database_model.Product(**product.model_dump())) - - db.commit() - - -init_db() +# Register routes +app.include_router(products_routes.router, + prefix=f"/api/{version}/products", tags=['Products']) +app.include_router(user_route.router, + prefix=f"/api/{version}/users", tags=['Users']) @app.get("/") def greet(): - return "Hello, World!" - - -@app.get("/products") -def get_all_products(db: Session = Depends(get_db)): - db_products = db.query(database_model.Product).all() - return db_products - - -@app.get("/products/{id}") -def get_product_by_id(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - return db_product - return "product not found" - - -@app.post("/products") -def add_product(product: Product, db: Session = Depends(get_db)): - db.add(database_model.Product(**product.model_dump())) - db.commit() - return product - - -@app.put("/products/{id}") -def update_product(id: int, product: Product, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db_product.name = product.name - db_product.description = product.description - db_product.price = product.price - db_product.quantity = product.quantity - db.commit() - return "product updated successfully" - else: - return "product not found" - - -@app.delete("/products/{id}") -def delete_product(id: int, db: Session = Depends(get_db)): - db_product = db.query(database_model.Product).filter( - database_model.Product.id == id).first() - if db_product: - db.delete(db_product) - db.commit() - return "product deleted successfully" - else: - return "product not found" + return {"message": "Hello, World!"} diff --git a/model.py b/model.py index 23192ce..9c4bd8b 100644 --- a/model.py +++ b/model.py @@ -1,8 +1,31 @@ from pydantic import BaseModel + class Product(BaseModel): - id:int - name:str - description:str - price:float - quantity:int \ No newline at end of file + id: int + name: str + description: str + price: float + quantity: int + + class Config: + from_attributes = True + + +class CreateUser(BaseModel): + username: str + password: str + + +class User(BaseModel): + id: int + username: str + password: str + + class Config: + from_attributes = True + + +class Token(BaseModel): + access_token: str + token_type: str diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..598dd488b4e7562b7aeb22bb539df23f35435785 GIT binary patch literal 162 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 Date: Wed, 8 Oct 2025 17:56:22 +0500 Subject: [PATCH 02/10] Add File Upload with User Authentication and Database Storage --- .gitignore | 1 + __pycache__/database.cpython-313.pyc | Bin 471 -> 437 bytes __pycache__/database_model.cpython-313.pyc | Bin 867 -> 1607 bytes __pycache__/main.cpython-313.pyc | Bin 5286 -> 1458 bytes __pycache__/model.cpython-313.pyc | Bin 613 -> 1668 bytes auth.py | 2 + database_model.py | 27 ++++++--- main.py | 4 +- model.py | 5 ++ requirement.txt | 10 +++ .../products_routes.cpython-313.pyc | Bin 4980 -> 4980 bytes routes/__pycache__/user_route.cpython-313.pyc | Bin 3602 -> 3602 bytes routes/file_routes.py | 57 ++++++++++++++++++ routes/user_route.py | 6 +- uploads/85b140b589bf4c79b5c61f4c7d7fb794.docx | Bin 0 -> 24037 bytes uploads/88000450138746dcae8b06ee5fd6e40a.txt | 57 ++++++++++++++++++ uploads/FASTAPI_NOTES.docx | Bin 0 -> 24037 bytes uploads/main.py | 56 +++++++++++++++++ 18 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 .gitignore create mode 100644 routes/file_routes.py create mode 100644 uploads/85b140b589bf4c79b5c61f4c7d7fb794.docx create mode 100644 uploads/88000450138746dcae8b06ee5fd6e40a.txt create mode 100644 uploads/FASTAPI_NOTES.docx create mode 100644 uploads/main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc index 714e5d917a25321defea4f9a3a6d91d8e170b3e1..6727ab55df928bd940c53b7f9698d2da9304f068 100644 GIT binary patch delta 72 zcmcc4yp@^zGcPX}0}xy~^LQh-7NfqEi&acoVsS}gL8fj>YHq%6K~a86X>v(1cG1ag GjDY}qHyPak delta 72 zcmdnWe4Uy5GcPX}0}$vc-P_2m#V8u%Y!wq)oLW>IQ=XX@dE3=We!FU; zQc^)67<>_gKzi%3x%FQurclkL4W$L&3RdrZZ&pg3#(@+%LT~4N%*>nL@4Y!G6tWD* z?N9&MyPIb0H&W)qm`zT;#>qqGFh|>9O%1eW3R2B9q?CeL}I9k}4e{uhGpxRGenHuGW5CMUZ%dB|D} zG>1XT(ID-lKyMi0)of|s zjs&dvfjG9&C$j@o`7Y|w_T!Yy^}+ADFyx<$p)~x^6L-F{DI}`<-ea~^D@vR5(CrG& zWtQ`9&l?0Z&vU*%aD$nr#BocA;=m6@*h6=L^Vr=9MB)eH0dzV)<$FLpOPg{aw=DF) z@A#n`sB^#~ucgxTJ-Ntv6eFN*?#77x&L9>M=Wq^#PVmp3X1OXJigW^8>xx75uoMcL4>$~kA%0FgSM>i*>#qsXp z-O>7_TzRzmU<>`l^7xZ)Kl{FNRK0RkY~1@0F3z_)oA^C+|4+o{=9EJ|DTJqb!7-rN zF>98zK2D%GF5$V-o&&QD1xk}3+^Ut}JSht#ULjE?u}I=6X|P19Vog_!`EJ+jr2NtUKVN9at5~Cb1jBVlv7QlQxhH2 zour|rZVFbr??%y=J@C#%?-HFrg3^fIL_uYG@j3LU zOjXgAR>A}iBL3ME_uAh3`K)ogUP3)tj zeH67S!)G~{_PfcGaUI^m-RUuluA%rdt!dgX?8Z;*#_#OyC)TS+)@vhuY8u+=K|E#n anHFvBjRSAW@H1U9wCcf?ztCP)%>NA`MtJ7{ delta 430 zcmX@k^O%kAGcPX}0}w>(+{-9ooX95;=>z0VWe8>nX7pz8Vk~0xVk%){h@;6ealOLC z+gg+VF;!@>0R?Vx6cpvBlqQ!HhiEe0;w~u4%uOt+jL%N3yv3TCmy%kslEF`tfAS{A zh{*?-H6|M{NksDjwS5K|b}Px*DkiizwWv6zJTouGCAB!aB)=dgEwQ*Hu^>}7B{er+ z7iPL{d1gt5ZfPcd*~yJe!d#p{ql-WU+hleYb#+1U8Eli~8oY0E2}}^})@^XT$tyg; zy*s|yqeu#-(od6favQ6{_Z z+lzZd7vz+Qt;M#&Lkf36`P*mO9T3%b$)|J+Tz;AT4D>S*WBdkPeuF;#9c2Ohfeyy# z;3S?tQh!rY!|F7@J6*kUDhq1yNStX1OB0kCqs&ywO!ci%=EJGJI^3M82-)57}8(#y416kIA3jeFwr;1`erou+9-eXICmU7pL_SMx%(4+W2|pnFcDsOf@IAA literal 5286 zcmdT{Yit|G5#GJyx7Nj4B9ejpAeWZ;b0D9Y@Kv6C-B=QT@!BR7D{}5$WS*teOC3jOek2`iELo)XOSl6jcZjx6{Xc#^ozvCc}*VJWECwBN_;}C0SLKWa61*Bplamaw4WAOR)^GQ`3Io#LRr_)Z5t_a8D$tyESrhu4g4i{u?J{eU(iDgzvL^lyJy6u^8N=v4e z8%~JxN^HJ6C9}CCTL>p2vKTj(V!=E9m;d(Ujj>G(@qkt5l&Ee=gcoGp6_wQpQ&Jk7 zRJW#>5|MS`@?tolDcaH|L_5))(n6SNsrh6=#y#R;O;$B=AsLn9qFP+a?{WX-vCRe$ z+T;z%aXB0{1l8rHsidOH-4Q%iJgy{S;(T#2HvZXi^{@UtKekzkvKExJpGzhbEyu-#)<`_5B8Ir=tcr&a_2*BGgr-%Qsi7-MB6M0-UjRRc<^X@; zl+qoQ7n0qnJl@pqD~dMXy{LS}>Oxpa^rV(_TTIqwqO+_6_Gy7fJqWK&vO+#~)cnl9 zax%y9R$tb$=cmF0!FfZtCcNtW$10a6{-ss}^G{sE^yii~@{6ir`mXl~{~LaoK4x17 zFrlrsMYzn)cHP;U7tt$53))Po)SLM#i2BQ#>4yZJrV<@_o{;dj2)Sr{*O1jm#)k+= zaIkMhnJ5f*-xt0WTpyX{f(X&FX6}2M4pH-bA~`oqh>IAQsVWnIp(%ryiID(5#W2`z zlYpZDhs%pHThcwzuoj*TtMZHya11R1P)RN(G+i*Auk+z(RCgL2GttF`lx~Y87ZwyP zz_a}zz`9V%ZnC3SL{Vf1j9CT3452UpqX0bWC3qEqkac@*j9(jn{X3bOL+P4B8}6=* zdm!x|*l-W7JoUitzDZwPzF)U*^(-=;xpwCD(Tp$fkuQ+;op|qTrfW3aHM-F?2BSZ@ zPj2y~Ud-8u!*#=P&GD)$=OBW28+Ic)?R^Y82!8`R2!&fDMg!c`a>Y0#j%1hkT$073 zEey|^e;r23xQsQ%WLab2WO5W96|=(%dEjbf$g}L|HBDfcC4$?S4YxD4{4Nl;Fo+*r zEpS~#9~muJ1gSX#E-gp|F`mns>28E(J7I0G9I&uX=ol6QdB5_I^dTLf%OEk&&X}iF zaa+@2doXl?vAD`cVD$-jR1}2kdaK_!_}am@M>9?R>8Ab-??A>ol=cp7c!yUev##28 zOKsj9o}xK=f1pHv#1N&C;tgSdcQkDBM;ed)%&jjH}k)o{9M_+z0q zBly#TKO?lHg_gBoTIks3sW)&x&~v-y{kj}atd)PYS7j?3)`fL{c-vKx*cPj!HdUD7e8z;>viri^e6SXmDotpt14i^D#60rv4ehK(P? zybJQ1X;yX$SkRU##xxkzVteJx)ic*eGp?48TrFwWv9+_A_Mvq9&_??(jP6=SvcU56 z)u&$q^m|vP48ujxMrJGG{1QYk*o^xI;hdl5i#VSKoYSEBvu557`e{C1{FDNR=aHoX z2^__&1sr>Jwr2ZUxs4ID)?t zd1o96yW>c=A!(GC3`5yS!MaDi91q7M^YX${5BxSV3}GlJ zQz_kpS+}HEx15AJR2SeUG>po0XN1WRvsjFobUwPU@f~5O11zvlegG*SYRJl=ao#+@G@(>f0g~%H`aIR%VDVO?(;B zoF>f~(vc<|?_F5OesD`5R`=@Vo8MpW9?yE~*9NwDYVG*U;)1y!X6p~%8Tt@PS!z8` zi*rZrT=;Mn=YoZ~rS+aslaAKr zg_j&qA9L1M>aNzU`d)0_;wWv}<_YJ?A(a!%wWbYPbFZ!Qy`guX+#(#Ud$aB#cF0v7&3p>!X{6g`u&;TKD$L+wC$#v!*1<4qx+!a wA&v@r0hMb;$;PXV>+a?ay7yjZ{|5)&@7*FoUJ|-B`$HU|h<}R%%1DI&0lUiQu>b%7 diff --git a/__pycache__/model.cpython-313.pyc b/__pycache__/model.cpython-313.pyc index 2dfe74a8f02eb6fda4dc1b4cafbe587b8c1aa2e7..486da57b21c4a80b285f1fc5b36c38847652e8ef 100644 GIT binary patch literal 1668 zcmb7EOKTKC5bk-;?&h6MOyVOUzF08<55WiO!3Y{PF);DA(2TpYo59_gwR(c&9Ebrg zUh*UIdvaMCJPE?$Mc5UuR`qN`Caj`;_`2q+>guZMs_707=NYa?o8Pv-Ym9xRkCSEf zh3+;Ahs|2T9P^B_wqm4qpexoy);hx)inWn-&akFpU1W1-SgW2d z6*}auq{-Y9Uj*xMHE2}Q3sN-uAKfwvhpfy%^BCx!2F7((Hp`Z$-$YH>M#(spOeHzU zSxO>1nWmF9$h_L^_)gS9dB&he{#!uqw^SZw9hZ4MYXPRuaaMv|12W z0_ncr@uMV6b~DjpP1Wko;FgWn)4}u%uF`fp=L!9G`%WC?ZHBj1q}C_)!#7kZ~k12SX$vty&}YlhnmD9UwocTPi8g%P1g0 z+(7uq+HPU*SvxVQlYJdt`iX{g)t|5_PsLQ zMg60?%QL~QTP0iOS0V6|;Ef2NGlqwyyMv-?7q{gL@gat4T7)BKP!ZrV9)e2*gYsM; z1p)aMRASpWG`}Sp zA75OOSdy6>pIA~-l$lgol3E-eU&I46?=#5ETanIIF`>n&Ma41YnRziTsm0kP`2~~t zn8Y}A%QH(dbW1ZqoO&21CKu!+y@JXjR-kS$!3<;+O9P2^h8yf$&2Be1d73?L2naX( z-{9hJ_P)U@(B@Ml22<^)$$X2WpfV*f59p{OkY0#MAQwSYi33@;IBb9xmFA?{6-ffQ hjEX>_SOiFXU}j`wyvrc>g@u(-up{#;1CRo10{~;=SULaz diff --git a/auth.py b/auth.py index 45a21e2..6206006 100644 --- a/auth.py +++ b/auth.py @@ -54,3 +54,5 @@ def get_current_user(token: str = Depends(oauth2_bearer), db: Session = Depends( except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token") + + diff --git a/database_model.py b/database_model.py index 30102fe..a5c2536 100644 --- a/database_model.py +++ b/database_model.py @@ -1,22 +1,31 @@ -from sqlalchemy import Column, Integer, String, Float +from sqlalchemy import Column, Integer, String, Float, ForeignKey + from sqlalchemy.ext.declarative import declarative_base -Base=declarative_base() +Base = declarative_base() + + +class UserFile(Base): + __tablename__ = "user_files" + id = Column(Integer, primary_key=True, index=True) + filename = Column(String) # safe unique name like "a1b2c3.jpg" + original_name = Column(String) # "my_photo.jpg" + user_id = Column(Integer, ForeignKey("users.id")) class Product(Base): - + __tablename__ = "products" id = Column(Integer, primary_key=True, index=True) name = Column(String) - description= Column(String) - price=Column(Float) - quantity=Column(Integer) + description = Column(String) + price = Column(Float) + quantity = Column(Integer) class User(Base): __tablename__ = "users" - id= Column(Integer,primary_key=True,index=True) - username=Column(String,unique=True,index=True) - password=Column(String) \ No newline at end of file + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + password = Column(String) diff --git a/main.py b/main.py index 98e85c3..c49d746 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from fastapi.middleware.cors import CORSMiddleware import database_model from database import engine -from routes import products_routes, user_route +from routes import file_routes, products_routes, user_route version = "v1" app = FastAPI(title="Fastapi ", @@ -20,6 +20,8 @@ # Register routes app.include_router(products_routes.router, prefix=f"/api/{version}/products", tags=['Products']) +app.include_router(file_routes.router, + prefix=f"/api/{version}/files", tags=['Files']) app.include_router(user_route.router, prefix=f"/api/{version}/users", tags=['Users']) diff --git a/model.py b/model.py index 9c4bd8b..2f11cce 100644 --- a/model.py +++ b/model.py @@ -1,6 +1,7 @@ from pydantic import BaseModel + class Product(BaseModel): id: int name: str @@ -21,6 +22,7 @@ class User(BaseModel): id: int username: str password: str + class Config: from_attributes = True @@ -29,3 +31,6 @@ class Config: class Token(BaseModel): access_token: str token_type: str + + + \ No newline at end of file diff --git a/requirement.txt b/requirement.txt index 96c59e6..f081a46 100644 --- a/requirement.txt +++ b/requirement.txt @@ -1,9 +1,13 @@ annotated-types==0.7.0 anyio==4.11.0 +bcrypt==4.0.1 certifi==2025.10.5 +cffi==2.0.0 click==8.3.0 colorama==0.4.6 +cryptography==46.0.2 dnspython==2.8.0 +ecdsa==0.19.1 email-validator==2.3.0 fastapi==0.118.0 fastapi-cli==0.0.13 @@ -18,19 +22,25 @@ Jinja2==3.1.6 markdown-it-py==4.0.0 MarkupSafe==3.0.3 mdurl==0.1.2 +passlib==1.7.4 psycopg2==2.9.10 psycopg2-binary==2.9.10 +pyasn1==0.6.1 +pycparser==2.23 pydantic==2.11.10 pydantic_core==2.33.2 Pygments==2.19.2 python-dotenv==1.1.1 +python-jose==3.5.0 python-multipart==0.0.20 PyYAML==6.0.3 rich==14.1.0 rich-toolkit==0.15.1 rignore==0.7.0 +rsa==4.9.1 sentry-sdk==2.40.0 shellingham==1.5.4 +six==1.17.0 sniffio==1.3.1 SQLAlchemy==2.0.43 sqlmodel==0.0.25 diff --git a/routes/__pycache__/products_routes.cpython-313.pyc b/routes/__pycache__/products_routes.cpython-313.pyc index bc9fc154e037312c5f6196f4c63f8c75becdb930..ed383cb3fcdd60d096cdc8a4417e3f8200dcd3e1 100644 GIT binary patch delta 20 acmeyO_C<~RGcPX}0}!kTc(#!{Qy2hAC<^h3H8%HiaWgWSZJy2jn2}Lsay@U6 iA{zq}N4s>RbVuZ6M)8kKjEs7pnHiWQz^o!|peX?6{uT59 delta 83 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 literal 0 HcmV?d00001 diff --git a/uploads/88000450138746dcae8b06ee5fd6e40a.txt b/uploads/88000450138746dcae8b06ee5fd6e40a.txt new file mode 100644 index 0000000..693a603 --- /dev/null +++ b/uploads/88000450138746dcae8b06ee5fd6e40a.txt @@ -0,0 +1,57 @@ +aiomysql==0.2.0 +alembic==1.16.5 +annotated-types==0.7.0 +anyio==4.11.0 +asyncpg==0.30.0 +bcrypt==5.0.0 +certifi==2025.8.3 +cffi==2.0.0 +click==8.3.0 +colorama==0.4.6 +cryptography==46.0.2 +dnspython==2.8.0 +ecdsa==0.19.1 +email-validator==2.3.0 +fastapi==0.118.0 +fastapi-cli==0.0.13 +fastapi-cloud-cli==0.2.1 +greenlet==3.2.4 +h11==0.16.0 +httpcore==1.0.9 +httptools==0.6.4 +httpx==0.28.1 +idna==3.10 +Jinja2==3.1.6 +Mako==1.3.10 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +passlib==1.7.4 +pyasn1==0.6.1 +pycparser==2.23 +pydantic==2.11.9 +pydantic_core==2.33.2 +Pygments==2.19.2 +PyMySQL==1.1.2 +python-decouple==3.8 +python-dotenv==1.1.1 +python-jose==3.5.0 +python-multipart==0.0.20 +PyYAML==6.0.3 +rich==14.1.0 +rich-toolkit==0.15.1 +rignore==0.6.4 +rsa==4.9.1 +sentry-sdk==2.39.0 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +SQLAlchemy==2.0.43 +starlette==0.48.0 +typer==0.19.2 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.37.0 +watchfiles==1.1.0 +websockets==15.0.1 diff --git a/uploads/FASTAPI_NOTES.docx b/uploads/FASTAPI_NOTES.docx new file mode 100644 index 0000000000000000000000000000000000000000..5e8f7201b3ae97b78788d360fcbdd4dff400dc60 GIT binary patch literal 24037 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 literal 0 HcmV?d00001 diff --git a/uploads/main.py b/uploads/main.py new file mode 100644 index 0000000..1146afd --- /dev/null +++ b/uploads/main.py @@ -0,0 +1,56 @@ +from fastapi import FastAPI, Response +from typing import Optional +from fastapi.params import Body +from pydantic import BaseModel +from random import randrange + +app = FastAPI() + + +class Post(BaseModel): + title: str + content: str + publish: bool = True + rating: Optional[int] = None + + +my_posts = [ + {"title": "title of post 1", "content": "content of post 1", "id": 1}, + {"title": "Fav anime", "content": "onpiece or naruto", "id": 2} +] + + +def find_post(id): + for p in my_posts: + if p['id'] == id: + return p + + +@app.get("/") +async def root(): + return {"message": "Hello World"} + + +@app.get('/posts') +def get_posts(): + return { + "data": my_posts + } + + +@app.post("/posts") +async def create_post(post: Post): + post_dict = post.dict() + post_dict['id'] = randrange(0, 1000000) + my_posts.append(post_dict) + return { + "data": post_dict + } + + +@app.get('/posts/{id}') +def get_post(id: int, response: Response): + post = find_post(id) + if not post: + response.status_code = 404 + return {"post_detail": post} From 212e3e29521987eaa8664ec48d37278d0ec50b45 Mon Sep 17 00:00:00 2001 From: abubakar Date: Thu, 9 Oct 2025 16:51:09 +0500 Subject: [PATCH 03/10] Add CRUD operations for Post entity with authentication support --- __pycache__/database_model.cpython-313.pyc | Bin 1607 -> 1607 bytes __pycache__/main.cpython-313.pyc | Bin 1458 -> 1482 bytes __pycache__/model.cpython-313.pyc | Bin 1668 -> 1668 bytes auth.py | 6 +- db/__init__.py | 0 database.py => db/database.py | 0 database_model.py => db/database_model.py | 9 +++ main.py | 21 +++--- .../products_routes.cpython-313.pyc | Bin 4980 -> 5014 bytes routes/__pycache__/user_route.cpython-313.pyc | Bin 3602 -> 3636 bytes routes/file_routes.py | 13 ++-- routes/post_routes.py | 63 ++++++++++++++++++ routes/products_routes.py | 6 +- routes/user_route.py | 12 ++-- schemas/__init__.py | 0 model.py => schemas/model.py | 9 ++- 16 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 db/__init__.py rename database.py => db/database.py (100%) rename database_model.py => db/database_model.py (83%) create mode 100644 routes/post_routes.py create mode 100644 schemas/__init__.py rename model.py => schemas/model.py (85%) diff --git a/__pycache__/database_model.cpython-313.pyc b/__pycache__/database_model.cpython-313.pyc index 4a7a5b452fb960ef4ad8b11aaf11326f086aeb8f..2367e17d40cdd73b61a6642b2947e8562a19a5fd 100644 GIT binary patch delta 52 zcmX@kbDW3wGcPX}0}!w!KFes>$eYQ`C@{I4xgSWfvutCOp1hBx4M@7OJ_CyAut@^| Dda@1! delta 52 zcmX@kbDW3wGcPX}0}zBpKFg@v$eYQ`$UnK9xgSWfvutCOn!Jyt4M@7OK4avYtivV^ E0EFufLI3~& diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc index c8493d2055f3edf3fd0d12cb94d5800b9fcade71..95b745160d60e11f132c0d7eb4741462941f7076 100644 GIT binary patch delta 709 zcmdnQeTtj+GcPX}0}#|SKF`=Ykyn!O%0zYLdN0-@Rt1J&hF}(NHZS%fb}*aOo5PE< zh|`O!h|7z+h}(;&hzBUnR>T{_sKgM=9?TKL8_a3RT*Mc{8YBlY6BPt0pz=YyAZ1Lk z81`UKE=xuQh8VseRV>onL28(y!Q3&td~OI`i#MRmdp_S6F-P%<5tcOR4#z5 z9Mj+!9z50w0`&_a>z`b}C>n{|K4GAG5hV4RnDzyWnleH}gTjvm2wtgzq~_e>L*sJ;l}!G@tjfm; z)Kc&-L+M-nnNvR1H4~eA*K`8N1*rB(Du$VXC;P6r5$*W?|t*W?>(6LqF$)_Z(UbFoEP@b z_SvakKo_T_wLT*hDA`Xn(=5FL0;l#fO_|BfEXy_(rU)+0R2w}6nxV3-(rg_w&DN}9 z;&uDjm?~97RS{YpuYgUgd$&81%6c%x^~cy+og&B3PE);(gpFpc-Eo_>*8gT{&Q^0k zXSOh%6>H30uW`3tPV_hPCr)DnLm1@$W1#$Ffq9`fr}U-CfC-=r+Xxn~OS-rW+v5v1 ze}KzN@1@NQj@36E-{ZTI-*{)Xqo~)YR{h|$@R#`cYVsiWMTELY$M8clPx=G3G$19mESR#%KjFu7IoFAt zS5D})x`FHYvCIv!QP4y~jGbOD&N;5z>ULb$_l_Lq@jcSmO`KUFNr@3l{zoi775cZn z_l#9UiA02k$Rmspx`N^rJoyHu0N-Kf5_b4!va~J(B45F{1Y<~yVZJgj9u4ygCmW*- Xp!_c&NE&H8CLg4c%+FIrk_`I;oIrkG diff --git a/__pycache__/model.cpython-313.pyc b/__pycache__/model.cpython-313.pyc index 486da57b21c4a80b285f1fc5b36c38847652e8ef..3790d7062aa5e08d77ee7111af16fad14d747408 100644 GIT binary patch delta 134 zcmZqSZQ7WE7cf#}dLQIe9XR29Vstk^mHuWt9S|v|vqOl$|`2)e%VEW-Vo8n;gg{A99W76Sm{m>@a; delta 134 zcmZqSZQ7WE7oj#}dLQJ$W*V29Vstk^mHuWtC!Nn{2_Fz$ia?CaWWmyvh)BErNd(&71)0Z0{z11$ys0ev8d diff --git a/auth.py b/auth.py index 6206006..bf4fd9b 100644 --- a/auth.py +++ b/auth.py @@ -2,8 +2,8 @@ from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from database import SessionLocal -import database_model +from db.database import SessionLocal +import db.database_model as database_model from passlib.context import CryptContext from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt @@ -54,5 +54,3 @@ def get_current_user(token: str = Depends(oauth2_bearer), db: Session = Depends( except JWTError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token") - - diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/database.py b/db/database.py similarity index 100% rename from database.py rename to db/database.py diff --git a/database_model.py b/db/database_model.py similarity index 83% rename from database_model.py rename to db/database_model.py index a5c2536..71baa17 100644 --- a/database_model.py +++ b/db/database_model.py @@ -12,6 +12,7 @@ class UserFile(Base): original_name = Column(String) # "my_photo.jpg" user_id = Column(Integer, ForeignKey("users.id")) + class Product(Base): __tablename__ = "products" @@ -29,3 +30,11 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True, index=True) password = Column(String) + + +class Post(Base): + __tablename__ = "posts" + + post_id = Column(Integer, primary_key=True, index=True) + title = Column(String) + description = Column(String) diff --git a/main.py b/main.py index c49d746..81d259a 100644 --- a/main.py +++ b/main.py @@ -1,31 +1,32 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -import database_model -from database import engine -from routes import file_routes, products_routes, user_route +import db.database_model as database_model +from db.database import engine +from routes import file_routes, post_routes, products_routes, user_route version = "v1" app = FastAPI(title="Fastapi ", description="this is learning project.", version=version,) -app.add_middleware( - CORSMiddleware, - allow_origins=["http://localhost:3000"], - allow_methods=["*"], -) database_model.Base.metadata.create_all(bind=engine) -# Register routes + app.include_router(products_routes.router, prefix=f"/api/{version}/products", tags=['Products']) app.include_router(file_routes.router, prefix=f"/api/{version}/files", tags=['Files']) app.include_router(user_route.router, prefix=f"/api/{version}/users", tags=['Users']) - +app.include_router(post_routes.router, + prefix=f"/api/{version}/posts", tags=["Posts"]) @app.get("/") def greet(): return {"message": "Hello, World!"} + + + + + \ No newline at end of file diff --git a/routes/__pycache__/products_routes.cpython-313.pyc b/routes/__pycache__/products_routes.cpython-313.pyc index ed383cb3fcdd60d096cdc8a4417e3f8200dcd3e1..2ea4cded6fee1c9702313020fa7436b4c9c32089 100644 GIT binary patch delta 558 zcmeyOHcg%PGcPX}0}y!mKhM}Zkyny&-b8h09WU-8ZUu&5hF~^t9xvV^UND>8o6n2C zh+lyrhDnLR6r>3ViUcN3ij@}(R+3^U5(-uZlEN{<2x*{VfgsJve2kLT{HBZ$E&7bX zs+LSKj7khKLNGBC#$dG=UK6HZbt#4{mMliN@uFb;c}$Y_g!F61h=I*u3D!b0LmX&^ zA;=8`&CrfvgPNg(W`-ot4AaR#cQ_N{4wyM?!MbSXNCVBW1errHJP`iTLo-8m^C6~J zyo_>_*9sWf@)jp&q~<0T>*eOBq~;W{0mJwfcS@38N@7W3Qetsx5j!ZBI6%UJNW$@8 u<&*yjbSee{wWd>cr9U}j`wyw9L^m%->RgW`P#ox2QLn^y>)VFUp3J7VAf delta 601 zcmbQH{zZ-VGcPX}0}!kTc$P7DBCjOltcmK*8bw^e48d&P++I9IJPHgzHoG^k7he&d z0z(Xw5`!s70}vGPPn;GjFA%IG#ZV*|tPCWDVuTRVK*jt)>XY{|O4jq4GD5WIGX|?# zGQ}_|F~kVM#7r22)na%|n1a=%7_wNh7~#f?fb>K3A(O!xVEu&5)Qk}Yo68ccC4y$K z7|>vSq8z3j!v-}(2h9u#pc%%<&VsN>a2(L=da&Va!MbRMO92fxr^s+nh_VFhp_wa_ z&Z?>Z5@fI@(=B$F)Xi6!Zu2tAPTnYBB+r_gpOTtW#0HeS#gUR&l9-fOoLa;V%#4hT_ZiggG8o-uki5^Jd7D9W^C`hIi~tvnTeAQF diff --git a/routes/__pycache__/user_route.cpython-313.pyc b/routes/__pycache__/user_route.cpython-313.pyc index 8613bf5670c697888b7b32f80a513a961b95ef31..61dd484270f7594535d94f6d1300d7053ba5290a 100644 GIT binary patch delta 306 zcmbOvvqgsQGcPX}0}y!mKhNOgn8+u=xM8Atw2_xUk$?h2FhelAx1g6$k&u^gk+7FY zk%$6A45Jc*DM$wp6p03NNHG+N#fS#!Puw8SC^qqeDysyT_e+~mY_b}oDyt-rYdSfA zQJ!DKlo6uMgfUnsMqqLsqdKcJNb%%VjGBxxo3Ak5U}ZGhyq5hT6Qla(elBiCM$^f& zxfe3ZPPXOoa^fvc&PdHoEY{1-Pf5)w;sn_ba(WRrkhsO2lBAcCSdy5OSe#nK0}>HL z5{U=vnY@}Ofze|!53f9z;Y9|+y9_FK8MG#w@_I5VO|IuHQsiJ@;%Jv{lqA|Wr~B4GuF z7)B)qQ;=#PC=v6nO&EiPV)!Qqa*9lz#wg4x1vlX!qZp&~<{OMRSQ#xg zZ)AVS#Hg`(G8Z=^quJ#7+zT0HCOh(Ysj%ker=;c-ae`b2@>CHwkhsN>l30?Mlvtcv u!~^6`KFgE9=ssDGSDs7rB7^2#2F2S9qLagUJsDLdFXSy^WScC)rvL! Date: Fri, 10 Oct 2025 18:58:08 +0500 Subject: [PATCH 04/10] Company & Product API --- .gitignore | 8 +- app/auth.py | 59 +++++++++++ app/controllers/auth_controller.py | 31 ++++++ app/controllers/company_controller.py | 46 +++++++++ app/controllers/product_controller.py | 63 ++++++++++++ .../controllers/user_controller.py | 0 {db => app}/database.py | 13 +++ app/main.py | 52 ++++++++++ app/models/company.py | 15 +++ {schemas => app/models}/model.py | 7 +- app/models/product.py | 15 +++ app/models/user.py | 13 +++ app/schemas/company.py | 18 ++++ app/schemas/model.py | 28 ++++++ app/schemas/product.py | 18 ++++ app/schemas/user.py | 15 +++ app/services/company_service.py | 60 +++++++++++ app/services/product_service.py | 47 +++++++++ app/services/user_service.py | 33 ++++++ auth.py | 56 ----------- db/database_model.py | 40 -------- main.py | 32 ------ routes/__init__.py | 0 routes/__pycache__/__init__.cpython-313.pyc | Bin 162 -> 0 bytes .../products_routes.cpython-313.pyc | Bin 5014 -> 0 bytes routes/__pycache__/user_route.cpython-313.pyc | Bin 3636 -> 0 bytes routes/file_routes.py | 60 ----------- routes/post_routes.py | 63 ------------ routes/products_routes.py | 94 ------------------ routes/user_route.py | 60 ----------- schemas/__init__.py | 0 uploads/85b140b589bf4c79b5c61f4c7d7fb794.docx | Bin 24037 -> 0 bytes uploads/88000450138746dcae8b06ee5fd6e40a.txt | 57 ----------- uploads/FASTAPI_NOTES.docx | Bin 24037 -> 0 bytes uploads/main.py | 56 ----------- 35 files changed, 536 insertions(+), 523 deletions(-) create mode 100644 app/auth.py create mode 100644 app/controllers/auth_controller.py create mode 100644 app/controllers/company_controller.py create mode 100644 app/controllers/product_controller.py rename db/__init__.py => app/controllers/user_controller.py (100%) rename {db => app}/database.py (57%) create mode 100644 app/main.py create mode 100644 app/models/company.py rename {schemas => app/models}/model.py (87%) create mode 100644 app/models/product.py create mode 100644 app/models/user.py create mode 100644 app/schemas/company.py create mode 100644 app/schemas/model.py create mode 100644 app/schemas/product.py create mode 100644 app/schemas/user.py create mode 100644 app/services/company_service.py create mode 100644 app/services/product_service.py create mode 100644 app/services/user_service.py delete mode 100644 auth.py delete mode 100644 db/database_model.py delete mode 100644 main.py delete mode 100644 routes/__init__.py delete mode 100644 routes/__pycache__/__init__.cpython-313.pyc delete mode 100644 routes/__pycache__/products_routes.cpython-313.pyc delete mode 100644 routes/__pycache__/user_route.cpython-313.pyc delete mode 100644 routes/file_routes.py delete mode 100644 routes/post_routes.py delete mode 100644 routes/products_routes.py delete mode 100644 routes/user_route.py delete mode 100644 schemas/__init__.py delete mode 100644 uploads/85b140b589bf4c79b5c61f4c7d7fb794.docx delete mode 100644 uploads/88000450138746dcae8b06ee5fd6e40a.txt delete mode 100644 uploads/FASTAPI_NOTES.docx delete mode 100644 uploads/main.py diff --git a/.gitignore b/.gitignore index ba0430d..3ccd43f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -__pycache__/ \ No newline at end of file +# Ignore Python cache files +__pycache__/ +*.pyc +*.pyo + +# Ignore init files if you don’t want them tracked +__init__.py diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..0449346 --- /dev/null +++ b/app/auth.py @@ -0,0 +1,59 @@ +from datetime import datetime, timedelta +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy.orm import Session +from app.models.user import User +from app.database import get_db +from fastapi.security import HTTPAuthorizationCredentials + + +SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") +bearer_scheme = HTTPBearer() + + +# ---------------- PASSWORD UTILS ---------------- # +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +# ---------------- TOKEN CREATION ---------------- # +def create_access_token(user_id: int): + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode = {"sub": str(user_id), "exp": expire} + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +# ---------------- VERIFY CURRENT USER ---------------- # +def get_current_user( + token: HTTPAuthorizationCredentials = Depends(bearer_scheme), + db: Session = Depends(get_db) +): + credential_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + try: + payload = jwt.decode(token.credentials, SECRET_KEY, + algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise credential_exception + except JWTError: + raise credential_exception + + user = db.query(User).filter(User.id == int(user_id)).first() + if user is None: + raise credential_exception + return user diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py new file mode 100644 index 0000000..d214a06 --- /dev/null +++ b/app/controllers/auth_controller.py @@ -0,0 +1,31 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.user import UserCreate, UserResponse +from app.services.user_service import UserService +from app.database import get_db +from app.auth import create_access_token, verify_password +from app.models.user import User +router = APIRouter() + + +@router.post("/register", response_model=UserResponse) +def register(user: UserCreate, db: Session = Depends(get_db)): + service = UserService(db) + try: + return service.create_user(user) + except ValueError as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + +@router.post("/token") +def login(user: UserCreate, db: Session = Depends(get_db)): + # ✅ Query using database model + db_user = db.query(User).filter(User.username == user.username).first() + + if not db_user or not verify_password(user.password, db_user.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") + + token = create_access_token(db_user.id) + return {"access_token": token, "token_type": "bearer"} diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py new file mode 100644 index 0000000..76d647f --- /dev/null +++ b/app/controllers/company_controller.py @@ -0,0 +1,46 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.schemas.company import CompanyCreate, CompanyResponse +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user + +router = APIRouter() + + +@router.post("/", response_model=CompanyResponse) +def create_company( + company: CompanyCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.create_company(current_user.id, company) + + +@router.get("/me", response_model=CompanyResponse) +def get_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.get_my_company(current_user.id) + + +@router.put("/me", response_model=CompanyResponse) +def edit_my_company( + company: CompanyCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.edit_company(current_user.id, company) + + +@router.delete("/me", dependencies=[Depends(get_current_user)]) +def delete_my_company( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = CompanyService(db) + return service.delete_company(current_user.id) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py new file mode 100644 index 0000000..3d06d85 --- /dev/null +++ b/app/controllers/product_controller.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.schemas.product import ProductCreate, ProductResponse +from app.services.product_service import ProductService +from app.services.company_service import CompanyService +from app.database import get_db +from app.auth import get_current_user + + +router = APIRouter() + + +@router.post("/", response_model=ProductResponse) +def create_product( + product: ProductCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + company_service = CompanyService(db) + product_service = ProductService(db) + company = company_service.get_my_company(current_user.id) + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="no company found") + + return product_service.create_product(company.id, product) + + +@router.get("/", response_model=list[ProductResponse]) +def list_product( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + product_service = ProductService(db) + return product_service.list_products() + + +@router.get("/{product_id}", response_model=ProductResponse) +def get_product_by_id( + product_id: int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + product_service = ProductService(db) + return product_service.get_product(product_id) + + +@router.put("/{product_id}", response_model=ProductResponse) +def update_product_by_id( + product_id: int, + product: ProductCreate, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + product_service = ProductService(db) + return product_service.update_product(product_id,product) + +@router.delete("/{product_id}", response_model=ProductResponse) +def delete_product_by_id( + product_id: int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + product_service = ProductService(db) + return product_service.delete_product(product_id) \ No newline at end of file diff --git a/db/__init__.py b/app/controllers/user_controller.py similarity index 100% rename from db/__init__.py rename to app/controllers/user_controller.py diff --git a/db/database.py b/app/database.py similarity index 57% rename from db/database.py rename to app/database.py index dad789c..fbb7968 100644 --- a/db/database.py +++ b/app/database.py @@ -1,7 +1,20 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base + + db_url = "postgresql://postgres:123@localhost:5432/inventory" engine = create_engine(db_url) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..f59d8a9 --- /dev/null +++ b/app/main.py @@ -0,0 +1,52 @@ +from fastapi import FastAPI +from app.database import Base, engine +from app.controllers import auth_controller, company_controller, product_controller + + +Base.metadata.create_all(bind=engine) +version = "v1" +app = FastAPI(title="Company & Product API", + description="in which user create their company", + version=version + ) + +app.include_router(auth_controller.router, + prefix=f"/api/{version}/auth", tags=["Auth"]) +app.include_router(company_controller.router, + prefix=f"/api/{version}/company", tags=["Company"]) +app.include_router(product_controller.router, + prefix=f"/api/{version}/product", tags=["Product"]) + + +@app.get("/") +def root(): + return {"message": "Welcome to Company API!"} + + +# from fastapi import FastAPI +# from fastapi.middleware.cors import CORSMiddleware +# import db.database_model as database_model +# from app.database import engine +# from routes import file_routes, post_routes, products_routes, user_route + +# version = "v1" +# app = FastAPI(title="Fastapi ", +# description="this is learning project.", +# version=version,) + + +# database_model.Base.metadata.create_all(bind=engine) + + +# app.include_router(products_routes.router, +# prefix=f"/api/{version}/products", tags=['Products']) +# app.include_router(file_routes.router, +# prefix=f"/api/{version}/files", tags=['Files']) +# app.include_router(user_route.router, +# prefix=f"/api/{version}/users", tags=['Users']) +# app.include_router(post_routes.router, +# prefix=f"/api/{version}/posts", tags=["Posts"]) + +# @app.get("/") +# def greet(): +# return {"message": "Hello, World!"} diff --git a/app/models/company.py b/app/models/company.py new file mode 100644 index 0000000..555bc5e --- /dev/null +++ b/app/models/company.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Company(Base): + __tablename__ = "companies" + + id =Column(Integer, primary_key=True, index=True) + name= Column(String) + location = Column(String) + user_id = Column(Integer, ForeignKey("users.id")) + + user= relationship("User", back_populates="company") + products= relationship("Product", back_populates="company") \ No newline at end of file diff --git a/schemas/model.py b/app/models/model.py similarity index 87% rename from schemas/model.py rename to app/models/model.py index 8908cad..1643234 100644 --- a/schemas/model.py +++ b/app/models/model.py @@ -1,5 +1,5 @@ from pydantic import BaseModel - +from typing import Optional class Product(BaseModel): id: int @@ -22,14 +22,13 @@ class User(BaseModel): username: str password: str - class Config: - from_attributes = True class Post(BaseModel): - post_id: int + post_id: Optional [int] title: str description: str + class Token(BaseModel): access_token: str token_type: str diff --git a/app/models/product.py b/app/models/product.py new file mode 100644 index 0000000..29bce74 --- /dev/null +++ b/app/models/product.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, String, Float, ForeignKey +from sqlalchemy.orm import relationship +from app.database import Base + + +class Product(Base): + __tablename__ = "products" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String) + price = Column(Float) + description = Column(String,nullable=True) + company_id = Column(Integer, ForeignKey("companies.id")) + + company = relationship("Company", back_populates="products") diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..4b4cdfb --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True) + password = Column(String) + + company= relationship("Company",back_populates="user", uselist=False) diff --git a/app/schemas/company.py b/app/schemas/company.py new file mode 100644 index 0000000..7fa504e --- /dev/null +++ b/app/schemas/company.py @@ -0,0 +1,18 @@ +from pydantic import BaseModel +from typing import List, Optional +from app.schemas.product import ProductResponse + + +class CompanyCreate(BaseModel): + name: str + location: str + + +class CompanyResponse(BaseModel): + id: int + name: str + location: str + products: List[ProductResponse] = [] + + class Config: + from_attributes = True diff --git a/app/schemas/model.py b/app/schemas/model.py new file mode 100644 index 0000000..8ce1d9c --- /dev/null +++ b/app/schemas/model.py @@ -0,0 +1,28 @@ +from pydantic import BaseModel +from typing import Optional + +class Product(BaseModel): + id: int + name: str + description: str + price: float + quantity: int + + class Config: + from_attributes = True + + + + + + + +class Post(BaseModel): + post_id: Optional [int] + title: str + description: str + + +class Token(BaseModel): + access_token: str + token_type: str diff --git a/app/schemas/product.py b/app/schemas/product.py new file mode 100644 index 0000000..77ed9f7 --- /dev/null +++ b/app/schemas/product.py @@ -0,0 +1,18 @@ +from typing import Optional +from pydantic import BaseModel + + +class ProductCreate(BaseModel): + name: str + price: float + description: Optional[str] = None + + +class ProductResponse(BaseModel): + id: int + name: str + price: float + description: Optional[str] = None + + class Config: + from_attributes = True diff --git a/app/schemas/user.py b/app/schemas/user.py new file mode 100644 index 0000000..0e5a05c --- /dev/null +++ b/app/schemas/user.py @@ -0,0 +1,15 @@ +from pydantic import BaseModel + + +class UserCreate(BaseModel): + username: str + password: str + + +class UserResponse(BaseModel): + id: int + username: str + password: str + + class Config: + from_attributes = True diff --git a/app/services/company_service.py b/app/services/company_service.py new file mode 100644 index 0000000..e0b6155 --- /dev/null +++ b/app/services/company_service.py @@ -0,0 +1,60 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.models.company import Company +from app.schemas.company import CompanyCreate + + +class CompanyService: + def __init__(self, db: Session): + self.db = db + + def create_company(self, user_id: int, company_data: CompanyCreate): + existing = self.db.query(Company).filter( + Company.user_id == user_id).first() + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="user already has a company") + + new_company = Company( + name=company_data.name, + location=company_data.location, + user_id=user_id + ) + self.db.add(new_company) + self.db.commit() + self.db.refresh(new_company) + return new_company + + def get_my_company(self, user_id: int): + company = self.db.query(Company).filter( + Company.user_id == user_id).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + return company + + def edit_company(self, user_id: int, company_data: CompanyCreate): + company = self.db.query(Company).filter( + Company.user_id == user_id + ).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + company.name = company_data.name + company.location = company_data.location + self.db.commit() + self.db.refresh(company) + return company + + def delete_company(self, user_id: int): + company = self.db.query(Company).filter( + Company.user_id == user_id + ).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + self.db.delete(company) + self.db.commit() + return {"detail": "company deleted"} diff --git a/app/services/product_service.py b/app/services/product_service.py new file mode 100644 index 0000000..c9db8c9 --- /dev/null +++ b/app/services/product_service.py @@ -0,0 +1,47 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from app.models.product import Product +from app.schemas.product import ProductCreate + + +class ProductService: + def __init__(self, db: Session): + self.db = db + + def create_product(self, company_id: int, product_data: ProductCreate): + new_product = Product( + name=product_data.name, + price=product_data.price, + description=product_data.description, + company_id=company_id + ) + self.db.add(new_product) + self.db.commit() + self.db.refresh(new_product) + return new_product + + def list_products(self): + products = self.db.query(Product).all() + return products + + def get_product(self, product_id: int): + product = self.db.query(Product).filter( + Product.id == product_id).first() + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + def update_product(self, product_id: int, product_data: ProductCreate): + product = self.get_product(product_id) + product.name = product_data.name + product.price = product_data.price + product.description = product_data.description + self.db.commit() + self.db.refresh(product) + return product + + def delete_product(self, product_id: int): + product = self.get_product(product_id) + self.db.delete(product) + self.db.commit() + return {"detail": "Product deleted"} diff --git a/app/services/user_service.py b/app/services/user_service.py new file mode 100644 index 0000000..9f6e614 --- /dev/null +++ b/app/services/user_service.py @@ -0,0 +1,33 @@ +from sqlalchemy.orm import Session +from app.schemas.user import UserCreate +from app.models.user import User +from app.auth import get_password_hash, verify_password, create_access_token +from fastapi import HTTPException, status + + +class UserService: + def __init__(self, db: Session): + self.db = db + + def create_user(self, user: UserCreate): + db_user = self.db.query(User).filter( + User.username == user.username).first() + if db_user: + raise ValueError("Username already registered") + hashed_pw = get_password_hash(user.password) + new_user = User(username=user.username, password=hashed_pw) + self.db.add(new_user) + self.db.commit() + self.db.refresh(new_user) + return new_user + +# def login_user(self, user: UserCreate): +# db_user = self.db.query(User).filter(User.username == user.username).first() +# if not db_user or not verify_password(user.password, db_user.password): +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail="Invalid credentials" +# ) + +# token = create_access_token(db_user.id) +# return {"access_token": token, "token_type": "bearer"} diff --git a/auth.py b/auth.py deleted file mode 100644 index bf4fd9b..0000000 --- a/auth.py +++ /dev/null @@ -1,56 +0,0 @@ -from datetime import datetime, timedelta -from typing import Annotated -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session -from db.database import SessionLocal -import db.database_model as database_model -from passlib.context import CryptContext -from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm -from jose import JWTError, jwt - - -SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 - -bcrypt_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -oauth2_bearer = OAuth2PasswordBearer(tokenUrl="/api/v1/users/token") - - -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - -# create token here - - -def create_access_token(username: str, user_id: int, expires_delta: timedelta = None): - encode = {"sub": username, "id": user_id} - expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)) - encode.update({"exp": expire}) - return jwt.encode(encode, SECRET_KEY, algorithm=ALGORITHM) - -# verify token here - - -def get_current_user(token: str = Depends(oauth2_bearer), db: Session = Depends(get_db)): - try: - payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) - username: str = payload.get("sub") - user_id: int = payload.get("id") - if username is None or user_id is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalidate credentials") - user = db.query(database_model.User).filter( - database_model.User.id == user_id).first() - if user is None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="user not found" - ) - return user - except JWTError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid Token") diff --git a/db/database_model.py b/db/database_model.py deleted file mode 100644 index 71baa17..0000000 --- a/db/database_model.py +++ /dev/null @@ -1,40 +0,0 @@ -from sqlalchemy import Column, Integer, String, Float, ForeignKey - -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - - -class UserFile(Base): - __tablename__ = "user_files" - id = Column(Integer, primary_key=True, index=True) - filename = Column(String) # safe unique name like "a1b2c3.jpg" - original_name = Column(String) # "my_photo.jpg" - user_id = Column(Integer, ForeignKey("users.id")) - - -class Product(Base): - - __tablename__ = "products" - - id = Column(Integer, primary_key=True, index=True) - name = Column(String) - description = Column(String) - price = Column(Float) - quantity = Column(Integer) - - -class User(Base): - __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - username = Column(String, unique=True, index=True) - password = Column(String) - - -class Post(Base): - __tablename__ = "posts" - - post_id = Column(Integer, primary_key=True, index=True) - title = Column(String) - description = Column(String) diff --git a/main.py b/main.py deleted file mode 100644 index 81d259a..0000000 --- a/main.py +++ /dev/null @@ -1,32 +0,0 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -import db.database_model as database_model -from db.database import engine -from routes import file_routes, post_routes, products_routes, user_route - -version = "v1" -app = FastAPI(title="Fastapi ", - description="this is learning project.", - version=version,) - - -database_model.Base.metadata.create_all(bind=engine) - - -app.include_router(products_routes.router, - prefix=f"/api/{version}/products", tags=['Products']) -app.include_router(file_routes.router, - prefix=f"/api/{version}/files", tags=['Files']) -app.include_router(user_route.router, - prefix=f"/api/{version}/users", tags=['Users']) -app.include_router(post_routes.router, - prefix=f"/api/{version}/posts", tags=["Posts"]) - -@app.get("/") -def greet(): - return {"message": "Hello, World!"} - - - - - \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/routes/__pycache__/__init__.cpython-313.pyc b/routes/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 598dd488b4e7562b7aeb22bb539df23f35435785..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 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 diff --git a/uploads/88000450138746dcae8b06ee5fd6e40a.txt b/uploads/88000450138746dcae8b06ee5fd6e40a.txt deleted file mode 100644 index 693a603..0000000 --- a/uploads/88000450138746dcae8b06ee5fd6e40a.txt +++ /dev/null @@ -1,57 +0,0 @@ -aiomysql==0.2.0 -alembic==1.16.5 -annotated-types==0.7.0 -anyio==4.11.0 -asyncpg==0.30.0 -bcrypt==5.0.0 -certifi==2025.8.3 -cffi==2.0.0 -click==8.3.0 -colorama==0.4.6 -cryptography==46.0.2 -dnspython==2.8.0 -ecdsa==0.19.1 -email-validator==2.3.0 -fastapi==0.118.0 -fastapi-cli==0.0.13 -fastapi-cloud-cli==0.2.1 -greenlet==3.2.4 -h11==0.16.0 -httpcore==1.0.9 -httptools==0.6.4 -httpx==0.28.1 -idna==3.10 -Jinja2==3.1.6 -Mako==1.3.10 -markdown-it-py==4.0.0 -MarkupSafe==3.0.3 -mdurl==0.1.2 -passlib==1.7.4 -pyasn1==0.6.1 -pycparser==2.23 -pydantic==2.11.9 -pydantic_core==2.33.2 -Pygments==2.19.2 -PyMySQL==1.1.2 -python-decouple==3.8 -python-dotenv==1.1.1 -python-jose==3.5.0 -python-multipart==0.0.20 -PyYAML==6.0.3 -rich==14.1.0 -rich-toolkit==0.15.1 -rignore==0.6.4 -rsa==4.9.1 -sentry-sdk==2.39.0 -shellingham==1.5.4 -six==1.17.0 -sniffio==1.3.1 -SQLAlchemy==2.0.43 -starlette==0.48.0 -typer==0.19.2 -typing-inspection==0.4.2 -typing_extensions==4.15.0 -urllib3==2.5.0 -uvicorn==0.37.0 -watchfiles==1.1.0 -websockets==15.0.1 diff --git a/uploads/FASTAPI_NOTES.docx b/uploads/FASTAPI_NOTES.docx deleted file mode 100644 index 5e8f7201b3ae97b78788d360fcbdd4dff400dc60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24037 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 diff --git a/uploads/main.py b/uploads/main.py deleted file mode 100644 index 1146afd..0000000 --- a/uploads/main.py +++ /dev/null @@ -1,56 +0,0 @@ -from fastapi import FastAPI, Response -from typing import Optional -from fastapi.params import Body -from pydantic import BaseModel -from random import randrange - -app = FastAPI() - - -class Post(BaseModel): - title: str - content: str - publish: bool = True - rating: Optional[int] = None - - -my_posts = [ - {"title": "title of post 1", "content": "content of post 1", "id": 1}, - {"title": "Fav anime", "content": "onpiece or naruto", "id": 2} -] - - -def find_post(id): - for p in my_posts: - if p['id'] == id: - return p - - -@app.get("/") -async def root(): - return {"message": "Hello World"} - - -@app.get('/posts') -def get_posts(): - return { - "data": my_posts - } - - -@app.post("/posts") -async def create_post(post: Post): - post_dict = post.dict() - post_dict['id'] = randrange(0, 1000000) - my_posts.append(post_dict) - return { - "data": post_dict - } - - -@app.get('/posts/{id}') -def get_post(id: int, response: Response): - post = find_post(id) - if not post: - response.status_code = 404 - return {"post_detail": post} From 0799d287ae7b6daffec70f7fdfcd0dfa79455196 Mon Sep 17 00:00:00 2001 From: abubakar Date: Mon, 13 Oct 2025 18:57:01 +0500 Subject: [PATCH 05/10] Implement Company & Product with JWT Authentication and PostgreSQL Integration with alembic --- .idea/.gitignore | 8 + .idea/dataSources.xml | 12 ++ .idea/fastapi-demo-products.iml | 18 +++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + alembic.ini | 147 ++++++++++++++++++ alembic/README | 1 + alembic/env.py | 87 +++++++++++ alembic/script.py.mako | 28 ++++ alembic/versions/25d8f59a1dde_initial.py | 32 ++++ ...e1389d54a4b4_add_type_column_to_company.py | 32 ++++ app/auth.py | 2 +- app/controllers/auth_controller.py | 6 +- app/main.py | 36 ----- app/models/company.py | 6 +- app/models/model.py | 34 ---- app/models/product.py | 6 +- app/models/user.py | 4 +- app/schemas/company.py | 9 +- app/schemas/model.py | 28 ---- app/schemas/product.py | 6 +- app/services/company_service.py | 2 + app/services/user_service.py | 11 -- 25 files changed, 415 insertions(+), 127 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/dataSources.xml create mode 100644 .idea/fastapi-demo-products.iml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 alembic.ini create mode 100644 alembic/README create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/25d8f59a1dde_initial.py create mode 100644 alembic/versions/e1389d54a4b4_add_type_column_to_company.py delete mode 100644 app/models/model.py delete mode 100644 app/schemas/model.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..d4a6e52 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:5432/postgres + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/fastapi-demo-products.iml b/.idea/fastapi-demo-products.iml new file mode 100644 index 0000000..a6b95ba --- /dev/null +++ b/.idea/fastapi-demo-products.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..db14680 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a7351ee --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..6bfda86 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,147 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts. +# this is typically a path given in POSIX (e.g. forward slashes) +# format, relative to the token %(here)s which refers to the location of this +# ini file +script_location = %(here)s/alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. for multiple paths, the path separator +# is defined by "path_separator" below. +prepend_sys_path = . + + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the tzdata library which can be installed by adding +# `alembic[tz]` to the pip requirements. +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to /versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "path_separator" +# below. +# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions + +# path_separator; This indicates what character is used to split lists of file +# paths, including version_locations and prepend_sys_path within configparser +# files such as alembic.ini. +# The default rendered in new alembic.ini files is "os", which uses os.pathsep +# to provide os-dependent path splitting. +# +# Note that in order to support legacy alembic.ini files, this default does NOT +# take place if path_separator is not present in alembic.ini. If this +# option is omitted entirely, fallback logic is as follows: +# +# 1. Parsing of the version_locations option falls back to using the legacy +# "version_path_separator" key, which if absent then falls back to the legacy +# behavior of splitting on spaces and/or commas. +# 2. Parsing of the prepend_sys_path option falls back to the legacy +# behavior of splitting on spaces, commas, or colons. +# +# Valid values for path_separator are: +# +# path_separator = : +# path_separator = ; +# path_separator = space +# path_separator = newline +# +# Use os.pathsep. Default configuration used for new projects. +path_separator = os + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# database URL. This is consumed by the user-maintained env.py script only. +# other means of configuring database URLs may be customized within the env.py +# file. +sqlalchemy.url = postgresql://postgres:123@localhost:5432/inventory + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module +# hooks = ruff +# ruff.type = module +# ruff.module = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Alternatively, use the exec runner to execute a binary found on your PATH +# hooks = ruff +# ruff.type = exec +# ruff.executable = ruff +# ruff.options = check --fix REVISION_SCRIPT_FILENAME + +# Logging configuration. This is also consumed by the user-maintained +# env.py script only. +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..635fec5 --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,87 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +from app.database import Base +from app.models import user, company, product +target_metadata = Base.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..1101630 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,28 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + """Upgrade schema.""" + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + """Downgrade schema.""" + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/25d8f59a1dde_initial.py b/alembic/versions/25d8f59a1dde_initial.py new file mode 100644 index 0000000..0e3973d --- /dev/null +++ b/alembic/versions/25d8f59a1dde_initial.py @@ -0,0 +1,32 @@ +"""initial + +Revision ID: 25d8f59a1dde +Revises: +Create Date: 2025-10-13 14:14:29.332627 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '25d8f59a1dde' +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/alembic/versions/e1389d54a4b4_add_type_column_to_company.py b/alembic/versions/e1389d54a4b4_add_type_column_to_company.py new file mode 100644 index 0000000..7475f33 --- /dev/null +++ b/alembic/versions/e1389d54a4b4_add_type_column_to_company.py @@ -0,0 +1,32 @@ +"""add type column to company + +Revision ID: e1389d54a4b4 +Revises: 25d8f59a1dde +Create Date: 2025-10-13 14:20:56.229293 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e1389d54a4b4' +down_revision: Union[str, Sequence[str], None] = '25d8f59a1dde' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('type', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('company', 'type') + # ### end Alembic commands ### diff --git a/app/auth.py b/app/auth.py index 0449346..d48bd3c 100644 --- a/app/auth.py +++ b/app/auth.py @@ -11,7 +11,7 @@ SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 30 +ACCESS_TOKEN_EXPIRE_MINUTES = 7 * 24 * 60 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") bearer_scheme = HTTPBearer() diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py index d214a06..a5a7e05 100644 --- a/app/controllers/auth_controller.py +++ b/app/controllers/auth_controller.py @@ -4,7 +4,7 @@ from app.services.user_service import UserService from app.database import get_db from app.auth import create_access_token, verify_password -from app.models.user import User +from app.models.user import User router = APIRouter() @@ -20,7 +20,7 @@ def register(user: UserCreate, db: Session = Depends(get_db)): @router.post("/token") def login(user: UserCreate, db: Session = Depends(get_db)): - # ✅ Query using database model + # Query using database model db_user = db.query(User).filter(User.username == user.username).first() if not db_user or not verify_password(user.password, db_user.password): @@ -28,4 +28,4 @@ def login(user: UserCreate, db: Session = Depends(get_db)): status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") token = create_access_token(db_user.id) - return {"access_token": token, "token_type": "bearer"} + return {"access_token": token} \ No newline at end of file diff --git a/app/main.py b/app/main.py index f59d8a9..d45247e 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,7 @@ from fastapi import FastAPI -from app.database import Base, engine from app.controllers import auth_controller, company_controller, product_controller -Base.metadata.create_all(bind=engine) version = "v1" app = FastAPI(title="Company & Product API", description="in which user create their company", @@ -16,37 +14,3 @@ prefix=f"/api/{version}/company", tags=["Company"]) app.include_router(product_controller.router, prefix=f"/api/{version}/product", tags=["Product"]) - - -@app.get("/") -def root(): - return {"message": "Welcome to Company API!"} - - -# from fastapi import FastAPI -# from fastapi.middleware.cors import CORSMiddleware -# import db.database_model as database_model -# from app.database import engine -# from routes import file_routes, post_routes, products_routes, user_route - -# version = "v1" -# app = FastAPI(title="Fastapi ", -# description="this is learning project.", -# version=version,) - - -# database_model.Base.metadata.create_all(bind=engine) - - -# app.include_router(products_routes.router, -# prefix=f"/api/{version}/products", tags=['Products']) -# app.include_router(file_routes.router, -# prefix=f"/api/{version}/files", tags=['Files']) -# app.include_router(user_route.router, -# prefix=f"/api/{version}/users", tags=['Users']) -# app.include_router(post_routes.router, -# prefix=f"/api/{version}/posts", tags=["Posts"]) - -# @app.get("/") -# def greet(): -# return {"message": "Hello, World!"} diff --git a/app/models/company.py b/app/models/company.py index 555bc5e..46a1da9 100644 --- a/app/models/company.py +++ b/app/models/company.py @@ -4,12 +4,14 @@ class Company(Base): - __tablename__ = "companies" + __tablename__ = "company" id =Column(Integer, primary_key=True, index=True) name= Column(String) location = Column(String) - user_id = Column(Integer, ForeignKey("users.id")) + type = Column(String, nullable=True) + user_id = Column(Integer, ForeignKey("user.id")) + user= relationship("User", back_populates="company") products= relationship("Product", back_populates="company") \ No newline at end of file diff --git a/app/models/model.py b/app/models/model.py deleted file mode 100644 index 1643234..0000000 --- a/app/models/model.py +++ /dev/null @@ -1,34 +0,0 @@ -from pydantic import BaseModel -from typing import Optional - -class Product(BaseModel): - id: int - name: str - description: str - price: float - quantity: int - - class Config: - from_attributes = True - - -class CreateUser(BaseModel): - username: str - password: str - - -class User(BaseModel): - id: int - username: str - password: str - - -class Post(BaseModel): - post_id: Optional [int] - title: str - description: str - - -class Token(BaseModel): - access_token: str - token_type: str diff --git a/app/models/product.py b/app/models/product.py index 29bce74..b610eb1 100644 --- a/app/models/product.py +++ b/app/models/product.py @@ -4,12 +4,12 @@ class Product(Base): - __tablename__ = "products" + __tablename__ = "product" id = Column(Integer, primary_key=True, index=True) name = Column(String) price = Column(Float) description = Column(String,nullable=True) - company_id = Column(Integer, ForeignKey("companies.id")) + company_id = Column(Integer, ForeignKey("company.id")) - company = relationship("Company", back_populates="products") + company = relationship("Company", back_populates="products") \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py index 4b4cdfb..0033b39 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -4,10 +4,10 @@ class User(Base): - __tablename__ = "users" + __tablename__ = "user" id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True) password = Column(String) - company= relationship("Company",back_populates="user", uselist=False) + company= relationship("Company",back_populates="user", uselist=False) \ No newline at end of file diff --git a/app/schemas/company.py b/app/schemas/company.py index 7fa504e..1e742dd 100644 --- a/app/schemas/company.py +++ b/app/schemas/company.py @@ -6,13 +6,14 @@ class CompanyCreate(BaseModel): name: str location: str - + type: Optional[str] = None class CompanyResponse(BaseModel): id: int name: str location: str + type: Optional[str] = None products: List[ProductResponse] = [] - - class Config: - from_attributes = True + # + # class Config: + # from_attributes = True diff --git a/app/schemas/model.py b/app/schemas/model.py deleted file mode 100644 index 8ce1d9c..0000000 --- a/app/schemas/model.py +++ /dev/null @@ -1,28 +0,0 @@ -from pydantic import BaseModel -from typing import Optional - -class Product(BaseModel): - id: int - name: str - description: str - price: float - quantity: int - - class Config: - from_attributes = True - - - - - - - -class Post(BaseModel): - post_id: Optional [int] - title: str - description: str - - -class Token(BaseModel): - access_token: str - token_type: str diff --git a/app/schemas/product.py b/app/schemas/product.py index 77ed9f7..f5f3a31 100644 --- a/app/schemas/product.py +++ b/app/schemas/product.py @@ -13,6 +13,6 @@ class ProductResponse(BaseModel): name: str price: float description: Optional[str] = None - - class Config: - from_attributes = True + # + # class Config: + # from_attributes = True diff --git a/app/services/company_service.py b/app/services/company_service.py index e0b6155..2f2a365 100644 --- a/app/services/company_service.py +++ b/app/services/company_service.py @@ -18,6 +18,7 @@ def create_company(self, user_id: int, company_data: CompanyCreate): new_company = Company( name=company_data.name, location=company_data.location, + type=company_data.type, user_id=user_id ) self.db.add(new_company) @@ -43,6 +44,7 @@ def edit_company(self, user_id: int, company_data: CompanyCreate): ) company.name = company_data.name company.location = company_data.location + company.type = company_data.type self.db.commit() self.db.refresh(company) return company diff --git a/app/services/user_service.py b/app/services/user_service.py index 9f6e614..8d362bb 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -20,14 +20,3 @@ def create_user(self, user: UserCreate): self.db.commit() self.db.refresh(new_user) return new_user - -# def login_user(self, user: UserCreate): -# db_user = self.db.query(User).filter(User.username == user.username).first() -# if not db_user or not verify_password(user.password, db_user.password): -# raise HTTPException( -# status_code=status.HTTP_401_UNAUTHORIZED, -# detail="Invalid credentials" -# ) - -# token = create_access_token(db_user.id) -# return {"access_token": token, "token_type": "bearer"} From 2648e4d08496f93db7f6a9d6d85e2bfd20685538 Mon Sep 17 00:00:00 2001 From: abubakar Date: Wed, 15 Oct 2025 17:54:21 +0500 Subject: [PATCH 06/10] Refactored project structure and add repository and use model in service and in repo --- .idea/data_source_mapping.xml | 6 ++ .idea/watcherTasks.xml | 25 +++++++ ...enamed_type_to_company_type_in_company_.py | 34 +++++++++ app/controllers/auth_controller.py | 19 ++++- app/controllers/company_controller.py | 6 +- app/controllers/product_controller.py | 9 +-- app/controllers/user_controller.py | 0 app/models/company.py | 2 +- app/repositories/company_repository.py | 63 +++++++++++++++++ app/repositories/product_repository.py | 45 ++++++++++++ app/repositories/user_repository.py | 32 +++++++++ app/schemas/company.py | 4 +- app/schemas/user.py | 6 +- app/services/company_service.py | 70 +++++-------------- app/services/product_service.py | 38 +++------- app/services/user_service.py | 31 ++++---- 16 files changed, 281 insertions(+), 109 deletions(-) create mode 100644 .idea/data_source_mapping.xml create mode 100644 .idea/watcherTasks.xml create mode 100644 alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py delete mode 100644 app/controllers/user_controller.py create mode 100644 app/repositories/company_repository.py create mode 100644 app/repositories/product_repository.py create mode 100644 app/repositories/user_repository.py diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml new file mode 100644 index 0000000..24daca9 --- /dev/null +++ b/.idea/data_source_mapping.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml new file mode 100644 index 0000000..5d8e01b --- /dev/null +++ b/.idea/watcherTasks.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py b/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py new file mode 100644 index 0000000..655051a --- /dev/null +++ b/alembic/versions/b6ea24863180_renamed_type_to_company_type_in_company_.py @@ -0,0 +1,34 @@ +"""renamed type to company_type in Company model + +Revision ID: b6ea24863180 +Revises: e1389d54a4b4 +Create Date: 2025-10-15 16:56:05.452033 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b6ea24863180' +down_revision: Union[str, Sequence[str], None] = 'e1389d54a4b4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('company_type', sa.String(), nullable=True)) + op.drop_column('company', 'type') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('company', sa.Column('type', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.drop_column('company', 'company_type') + # ### end Alembic commands ### diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py index a5a7e05..97d4af1 100644 --- a/app/controllers/auth_controller.py +++ b/app/controllers/auth_controller.py @@ -3,7 +3,7 @@ from app.schemas.user import UserCreate, UserResponse from app.services.user_service import UserService from app.database import get_db -from app.auth import create_access_token, verify_password +from app.auth import create_access_token, verify_password, get_password_hash, get_current_user from app.models.user import User router = APIRouter() @@ -11,8 +11,9 @@ @router.post("/register", response_model=UserResponse) def register(user: UserCreate, db: Session = Depends(get_db)): service = UserService(db) + hashed_pw= get_password_hash(user.password) try: - return service.create_user(user) + return service.create_user(user.username, hashed_pw) except ValueError as e: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @@ -28,4 +29,16 @@ def login(user: UserCreate, db: Session = Depends(get_db)): status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials") token = create_access_token(db_user.id) - return {"access_token": token} \ No newline at end of file + return {"access_token": token} + +@router.get("/me", response_model=UserResponse) +def me( + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + service = UserService(db) + return service.get_user(current_user.id) + +@router.delete("/delete",dependencies=[Depends(get_current_user)]) +def delete( db: Session = Depends(get_db), current_user=Depends(get_current_user)): + service = UserService(db) + return service.delete_user(current_user.id) diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py index 76d647f..3dafc3f 100644 --- a/app/controllers/company_controller.py +++ b/app/controllers/company_controller.py @@ -15,7 +15,7 @@ def create_company( current_user=Depends(get_current_user) ): service = CompanyService(db) - return service.create_company(current_user.id, company) + return service.create_company(current_user.id, company.name,company.company_type, company.location) @router.get("/me", response_model=CompanyResponse) @@ -24,7 +24,7 @@ def get_my_company( current_user=Depends(get_current_user) ): service = CompanyService(db) - return service.get_my_company(current_user.id) + return service.get_company(current_user.id) @router.put("/me", response_model=CompanyResponse) @@ -34,7 +34,7 @@ def edit_my_company( current_user=Depends(get_current_user) ): service = CompanyService(db) - return service.edit_company(current_user.id, company) + return service.edit_company(current_user.id, company.name, company.company_type, company.location) @router.delete("/me", dependencies=[Depends(get_current_user)]) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py index 3d06d85..a8d092a 100644 --- a/app/controllers/product_controller.py +++ b/app/controllers/product_controller.py @@ -18,12 +18,12 @@ def create_product( ): company_service = CompanyService(db) product_service = ProductService(db) - company = company_service.get_my_company(current_user.id) + company = company_service.get_company(current_user.id) if not company: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="no company found") - return product_service.create_product(company.id, product) + return product_service.create_product(company.id, product.name,product.price,product.description) @router.get("/", response_model=list[ProductResponse]) @@ -52,12 +52,13 @@ def update_product_by_id( db: Session = Depends(get_db), current_user=Depends(get_current_user)): product_service = ProductService(db) - return product_service.update_product(product_id,product) + return product_service.update_product(product_id,product.name,product.price,product.description) @router.delete("/{product_id}", response_model=ProductResponse) def delete_product_by_id( product_id: int, db: Session = Depends(get_db), - current_user=Depends(get_current_user)): +current_user=Depends(get_current_user) + ): product_service = ProductService(db) return product_service.delete_product(product_id) \ No newline at end of file diff --git a/app/controllers/user_controller.py b/app/controllers/user_controller.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/company.py b/app/models/company.py index 46a1da9..2c4b50a 100644 --- a/app/models/company.py +++ b/app/models/company.py @@ -9,7 +9,7 @@ class Company(Base): id =Column(Integer, primary_key=True, index=True) name= Column(String) location = Column(String) - type = Column(String, nullable=True) + company_type = Column(String, nullable=True) user_id = Column(Integer, ForeignKey("user.id")) diff --git a/app/repositories/company_repository.py b/app/repositories/company_repository.py new file mode 100644 index 0000000..2f7124c --- /dev/null +++ b/app/repositories/company_repository.py @@ -0,0 +1,63 @@ +from fastapi import HTTPException,status +from sqlalchemy.orm import Session +from app.models.company import Company + + +class CompanyRepository: + def __init__(self ,db: Session): + self.db = db + + + + def create_my_company(self, user_id: int, name: str, company_type: str, location: str): + existed=self.db.query(Company).filter(Company.user_id == user_id).first() + company = Company(user_id=user_id, name=name, company_type=company_type, location=location) + if existed: + raise HTTPException(status_code=400, detail="Company already exists") + if not existed: + self.db.add(company) + self.db.commit() + self.db.refresh(company) + return company + + + + + def get_my_company(self, user_id: int): + company = self.db.query(Company).filter(Company.user_id == user_id).first() + if not company: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + return company + + + + def edit_my_company(self, user_id: int,name: str, company_type: str, location: str ): + existed = self.db.query(Company).filter(Company.user_id == user_id).first() + if not existed: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + if existed: + self.db.query(Company).filter(Company.user_id == user_id).update( + {"name": name, "company_type": company_type, "location": location} + ) + self.db.commit() + self.db.refresh(existed) + return existed + + + def delete_my_company(self, user_id: int): + existed = self.db.query(Company).filter(Company.user_id == user_id).first() + if not existed: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="company not found" + ) + if existed: + self.db.query(Company).filter(Company.user_id == user_id).delete() + self.db.commit() + self.db.refresh(existed) + return existed + + diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py new file mode 100644 index 0000000..c2deccf --- /dev/null +++ b/app/repositories/product_repository.py @@ -0,0 +1,45 @@ +from fastapi import HTTPException +from sqlalchemy.orm import Session +from app.models.product import Product + +class ProductRepository: + def __init__(self, db: Session): + self.db = db + + def create_my_product(self,company_id:int, name: str, price: float, description: str) -> Product: + product = Product(company_id,name=name, price=price, description=description) + self.db.add(product) + self.db.commit() + self.db.refresh(product) + return product + + def list_my_product(self): + products = self.db.query(Product).all() + return products + + def get_product_by_id(self, product_id: int): + product= self.db.query(Product).filter(Product.id == product_id).first() + if not product: + raise HTTPException(status_code=404, detail="Product not found") + return product + + def update_my_product(self, product_id: int, name: str, price: float, description: str): + product = self.get_product_by_id(product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + if product: + self.db.query(Product).filter(Product.id == product_id).update({"name": name, "price": price, "description": description}) + self.db.commit() + self.db.refresh(product) + return product + + + def delete_my_product(self, product_id: int): + product = self.get_product_by_id(product_id) + if not product: + raise HTTPException(status_code=404, detail="Product not found") + if product: + self.db.query(Product).filter(Product.id == product_id).delete() + self.db.commit() + self.db.refresh(product) + return product \ No newline at end of file diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py new file mode 100644 index 0000000..68e89f8 --- /dev/null +++ b/app/repositories/user_repository.py @@ -0,0 +1,32 @@ +from sqlalchemy.orm import Session +from app.models.user import User +from fastapi import HTTPException,status + +class UserRepository: + def __init__(self, db: Session): + self.db = db + + + def get_by_me(self, user_id: int): + user = self.db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="User not found") + return user + + def create_user(self, username: str, hashed_pw: str): + user = self.db.query(User).filter(User.username == username).first() + if not user: + user = User(username=username, password=hashed_pw) + self.db.add(user) + self.db.commit() + self.db.refresh(user) + return user + + def delete_user(self, user_id: int): + user = self.db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="User not found") + if user: + self.db.delete(user) + self.db.commit() + return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/app/schemas/company.py b/app/schemas/company.py index 1e742dd..34a7b77 100644 --- a/app/schemas/company.py +++ b/app/schemas/company.py @@ -6,13 +6,13 @@ class CompanyCreate(BaseModel): name: str location: str - type: Optional[str] = None + company_type: Optional[str] = None class CompanyResponse(BaseModel): id: int name: str location: str - type: Optional[str] = None + company_type: Optional[str] = None products: List[ProductResponse] = [] # # class Config: diff --git a/app/schemas/user.py b/app/schemas/user.py index 0e5a05c..af0c3cb 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -9,7 +9,11 @@ class UserCreate(BaseModel): class UserResponse(BaseModel): id: int username: str - password: str + class Config: from_attributes = True + + +class TokenResponse(BaseModel): + access_token: str \ No newline at end of file diff --git a/app/services/company_service.py b/app/services/company_service.py index 2f2a365..37e2476 100644 --- a/app/services/company_service.py +++ b/app/services/company_service.py @@ -1,62 +1,28 @@ from fastapi import HTTPException, status from sqlalchemy.orm import Session -from app.models.company import Company -from app.schemas.company import CompanyCreate +from app.repositories.company_repository import CompanyRepository + class CompanyService: def __init__(self, db: Session): - self.db = db - - def create_company(self, user_id: int, company_data: CompanyCreate): - existing = self.db.query(Company).filter( - Company.user_id == user_id).first() - if existing: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="user already has a company") - - new_company = Company( - name=company_data.name, - location=company_data.location, - type=company_data.type, - user_id=user_id - ) - self.db.add(new_company) - self.db.commit() - self.db.refresh(new_company) - return new_company - - def get_my_company(self, user_id: int): - company = self.db.query(Company).filter( - Company.user_id == user_id).first() - if not company: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") - return company + self.repo = CompanyRepository(db) + + def create_company(self, user_id: int,name: str, company_type: str, location: str): - def edit_company(self, user_id: int, company_data: CompanyCreate): - company = self.db.query(Company).filter( - Company.user_id == user_id - ).first() - if not company: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="company not found" - ) - company.name = company_data.name - company.location = company_data.location - company.type = company_data.type - self.db.commit() - self.db.refresh(company) + existed = self.repo.create_my_company(user_id, name, company_type, location) + if not existed: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + return existed + + def get_company(self, user_id: int): + company = self.repo.get_my_company(user_id) return company + def edit_company(self, user_id: int, name: str, company_type: str, location: str): + updated =self.repo.edit_my_company(user_id, name, company_type, location) + return updated + def delete_company(self, user_id: int): - company = self.db.query(Company).filter( - Company.user_id == user_id - ).first() - if not company: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="company not found" - ) - self.db.delete(company) - self.db.commit() - return {"detail": "company deleted"} + deleted = self.repo.delete_my_company(user_id) + return deleted diff --git a/app/services/product_service.py b/app/services/product_service.py index c9db8c9..f95da7a 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -1,47 +1,29 @@ from fastapi import HTTPException from sqlalchemy.orm import Session from app.models.product import Product +from app.repositories.product_repository import ProductRepository from app.schemas.product import ProductCreate class ProductService: def __init__(self, db: Session): - self.db = db + self.repo = ProductRepository(db) - def create_product(self, company_id: int, product_data: ProductCreate): - new_product = Product( - name=product_data.name, - price=product_data.price, - description=product_data.description, - company_id=company_id - ) - self.db.add(new_product) - self.db.commit() - self.db.refresh(new_product) - return new_product + def create_product(self, company_id: int,name: str,price: float,description: str): + created = self.repo.create_my_product(company_id,name,price,description) + return created def list_products(self): - products = self.db.query(Product).all() + products = self.repo.list_my_product() return products def get_product(self, product_id: int): - product = self.db.query(Product).filter( - Product.id == product_id).first() - if not product: - raise HTTPException(status_code=404, detail="Product not found") + product = self.repo.get_product_by_id(product_id) return product - def update_product(self, product_id: int, product_data: ProductCreate): - product = self.get_product(product_id) - product.name = product_data.name - product.price = product_data.price - product.description = product_data.description - self.db.commit() - self.db.refresh(product) + def update_product(self, product_id: int, name: str,price: float,description: str): + product = self.repo.update_my_product(product_id,name,price,description) return product def delete_product(self, product_id: int): - product = self.get_product(product_id) - self.db.delete(product) - self.db.commit() - return {"detail": "Product deleted"} + product = self.repo.delete_my_product(product_id) \ No newline at end of file diff --git a/app/services/user_service.py b/app/services/user_service.py index 8d362bb..bc5cdc1 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -1,22 +1,23 @@ from sqlalchemy.orm import Session -from app.schemas.user import UserCreate -from app.models.user import User -from app.auth import get_password_hash, verify_password, create_access_token from fastapi import HTTPException, status +from app.repositories.user_repository import UserRepository class UserService: def __init__(self, db: Session): - self.db = db + self.repo = UserRepository(db) - def create_user(self, user: UserCreate): - db_user = self.db.query(User).filter( - User.username == user.username).first() - if db_user: - raise ValueError("Username already registered") - hashed_pw = get_password_hash(user.password) - new_user = User(username=user.username, password=hashed_pw) - self.db.add(new_user) - self.db.commit() - self.db.refresh(new_user) - return new_user + def create_user(self, username: str, hashed_pw: str): + if self.repo.create_user(username, hashed_pw): + raise HTTPException(status_code=status.HTTP_201_CREATED, detail="User created successfully") + + return self.repo.create_user(username, hashed_pw) + + def get_user(self, user_id: int): + me = self.repo.get_by_me(user_id) + return me + + + def delete_user(self, user_id: int): + me = self.repo.delete_user(user_id) + return me From 73570b7d664e4e18074c9579784be0a4a9d8c77e Mon Sep 17 00:00:00 2001 From: abubakar Date: Thu, 16 Oct 2025 18:29:24 +0500 Subject: [PATCH 07/10] in which i add fields and dependency and use repo in service --- app/repositories/company_repository.py | 5 ++--- app/repositories/product_repository.py | 6 +++--- app/repositories/user_repository.py | 2 +- app/schemas/product.py | 8 ++++---- app/schemas/user.py | 4 ++-- app/services/product_service.py | 4 +--- 6 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/repositories/company_repository.py b/app/repositories/company_repository.py index 2f7124c..6ba133a 100644 --- a/app/repositories/company_repository.py +++ b/app/repositories/company_repository.py @@ -17,7 +17,6 @@ def create_my_company(self, user_id: int, name: str, company_type: str, location if not existed: self.db.add(company) self.db.commit() - self.db.refresh(company) return company @@ -44,7 +43,7 @@ def edit_my_company(self, user_id: int,name: str, company_type: str, location: s {"name": name, "company_type": company_type, "location": location} ) self.db.commit() - self.db.refresh(existed) + return existed @@ -57,7 +56,7 @@ def delete_my_company(self, user_id: int): if existed: self.db.query(Company).filter(Company.user_id == user_id).delete() self.db.commit() - self.db.refresh(existed) + return existed diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py index c2deccf..93d1bf1 100644 --- a/app/repositories/product_repository.py +++ b/app/repositories/product_repository.py @@ -10,7 +10,7 @@ def create_my_product(self,company_id:int, name: str, price: float, description: product = Product(company_id,name=name, price=price, description=description) self.db.add(product) self.db.commit() - self.db.refresh(product) + return product def list_my_product(self): @@ -30,7 +30,7 @@ def update_my_product(self, product_id: int, name: str, price: float, descriptio if product: self.db.query(Product).filter(Product.id == product_id).update({"name": name, "price": price, "description": description}) self.db.commit() - self.db.refresh(product) + return product @@ -41,5 +41,5 @@ def delete_my_product(self, product_id: int): if product: self.db.query(Product).filter(Product.id == product_id).delete() self.db.commit() - self.db.refresh(product) + return product \ No newline at end of file diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py index 68e89f8..0f8ec8c 100644 --- a/app/repositories/user_repository.py +++ b/app/repositories/user_repository.py @@ -19,7 +19,7 @@ def create_user(self, username: str, hashed_pw: str): user = User(username=username, password=hashed_pw) self.db.add(user) self.db.commit() - self.db.refresh(user) + return user def delete_user(self, user_id: int): diff --git a/app/schemas/product.py b/app/schemas/product.py index f5f3a31..e74b1b1 100644 --- a/app/schemas/product.py +++ b/app/schemas/product.py @@ -1,11 +1,11 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel,Field class ProductCreate(BaseModel): - name: str - price: float - description: Optional[str] = None + name: str = Field(..., min_length=3, max_length=50, description="Product name") + price: float = Field(..., gt=0, description="Price must be greater than 0") + description: Optional[str] = Field(None, max_length=200) class ProductResponse(BaseModel): diff --git a/app/schemas/user.py b/app/schemas/user.py index af0c3cb..f72d10b 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,8 +1,8 @@ -from pydantic import BaseModel +from pydantic import BaseModel,Field class UserCreate(BaseModel): - username: str + username: str = Field(..., min_length=3, max_length=15, description="User's unique name") password: str diff --git a/app/services/product_service.py b/app/services/product_service.py index f95da7a..8ae43c4 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -1,8 +1,6 @@ -from fastapi import HTTPException from sqlalchemy.orm import Session -from app.models.product import Product from app.repositories.product_repository import ProductRepository -from app.schemas.product import ProductCreate + class ProductService: From d7b9c331a77d8e916da84d26b17c58e8275595c4 Mon Sep 17 00:00:00 2001 From: abubakar Date: Mon, 20 Oct 2025 18:08:03 +0500 Subject: [PATCH 08/10] in which i create todo crud and also add middleware --- alembic/env.py | 2 +- .../bdd3c45ac166_create_todo_table.py | 41 ++++++++++++++++ app/controllers/todo_controller.py | 48 +++++++++++++++++++ app/main.py | 7 ++- app/middlewares/time_middleware.py | 13 +++++ app/models/todo.py | 14 ++++++ app/repositories/product_repository.py | 2 +- app/repositories/todo_repository.py | 48 +++++++++++++++++++ app/schemas/todo.py | 27 +++++++++++ app/services/company_service.py | 2 - app/services/todo_service.py | 23 +++++++++ 11 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 alembic/versions/bdd3c45ac166_create_todo_table.py create mode 100644 app/controllers/todo_controller.py create mode 100644 app/middlewares/time_middleware.py create mode 100644 app/models/todo.py create mode 100644 app/repositories/todo_repository.py create mode 100644 app/schemas/todo.py create mode 100644 app/services/todo_service.py diff --git a/alembic/env.py b/alembic/env.py index 635fec5..95c49bd 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -25,7 +25,7 @@ # from myapp import mymodel # target_metadata = mymodel.Base.metadata from app.database import Base -from app.models import user, company, product +from app.models import user, company, product,todo target_metadata = Base.metadata diff --git a/alembic/versions/bdd3c45ac166_create_todo_table.py b/alembic/versions/bdd3c45ac166_create_todo_table.py new file mode 100644 index 0000000..c7efc09 --- /dev/null +++ b/alembic/versions/bdd3c45ac166_create_todo_table.py @@ -0,0 +1,41 @@ +"""create todo table + +Revision ID: bdd3c45ac166 +Revises: b6ea24863180 +Create Date: 2025-10-20 14:24:05.987653 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'bdd3c45ac166' +down_revision: Union[str, Sequence[str], None] = 'b6ea24863180' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('published', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_todos_id'), table_name='todos') + op.drop_table('todos') + # ### end Alembic commands ### diff --git a/app/controllers/todo_controller.py b/app/controllers/todo_controller.py new file mode 100644 index 0000000..aad0637 --- /dev/null +++ b/app/controllers/todo_controller.py @@ -0,0 +1,48 @@ +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from app.auth import get_current_user +from app.database import get_db +from app.schemas.todo import TodoResponse,TodoRequest + +from app.services.todo_service import TodoService + +router = APIRouter() + +@router.get("/", response_model=List[TodoResponse]) +def get_all( +db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + todo_service = TodoService(db) + return todo_service.get_all_todo() + + +@router.post("/", response_model=TodoResponse) +def create(todo : TodoRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + + todo_service = TodoService(db) + return todo_service.create_todo(todo.title,todo.description,todo.published) + + + +@router.put("/{todo_id}", response_model=TodoResponse) +def update(todo_id:int, + todo : TodoRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + + todo_service = TodoService(db) + return todo_service.update_product(todo_id,todo.title,todo.description,todo.published) + + + +@router.delete("/{todo_id}", response_model=TodoResponse) +def delete(todo_id:int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user)): + + todo_service = TodoService(db) + return todo_service.delete_product(todo_id) \ No newline at end of file diff --git a/app/main.py b/app/main.py index d45247e..6f199d4 100644 --- a/app/main.py +++ b/app/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI -from app.controllers import auth_controller, company_controller, product_controller - +from app.controllers import auth_controller, company_controller, product_controller, todo_controller +from app.middlewares.time_middleware import add_process_time_header version = "v1" app = FastAPI(title="Company & Product API", @@ -8,9 +8,12 @@ version=version ) +app.middleware("")(add_process_time_header) app.include_router(auth_controller.router, prefix=f"/api/{version}/auth", tags=["Auth"]) app.include_router(company_controller.router, prefix=f"/api/{version}/company", tags=["Company"]) app.include_router(product_controller.router, prefix=f"/api/{version}/product", tags=["Product"]) +app.include_router(todo_controller.router, + prefix=f"/api/{version}/todo", tags=["Todo"]) diff --git a/app/middlewares/time_middleware.py b/app/middlewares/time_middleware.py new file mode 100644 index 0000000..9150207 --- /dev/null +++ b/app/middlewares/time_middleware.py @@ -0,0 +1,13 @@ +import time + +from fastapi import Request, APIRouter + +router = APIRouter() + + +async def add_process_time_header(request: Request, call_next): + start_time = time.perf_counter() + response = await call_next(request) + process_time = time.perf_counter() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response diff --git a/app/models/todo.py b/app/models/todo.py new file mode 100644 index 0000000..afbbe2b --- /dev/null +++ b/app/models/todo.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, func +from app.database import Base + +class Todo(Base): + __tablename__ = "todos" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String(100), nullable=False) + description = Column(String(500), nullable=True) + published = Column(Boolean, default=True, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + + diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py index 93d1bf1..0d63a04 100644 --- a/app/repositories/product_repository.py +++ b/app/repositories/product_repository.py @@ -7,7 +7,7 @@ def __init__(self, db: Session): self.db = db def create_my_product(self,company_id:int, name: str, price: float, description: str) -> Product: - product = Product(company_id,name=name, price=price, description=description) + product = Product(company_id=company_id, name=name, price=price, description=description) self.db.add(product) self.db.commit() diff --git a/app/repositories/todo_repository.py b/app/repositories/todo_repository.py new file mode 100644 index 0000000..d158f8c --- /dev/null +++ b/app/repositories/todo_repository.py @@ -0,0 +1,48 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session +from app.models.todo import Todo + + +class TodoRepository: + def __init__(self, db: Session): + self.db = db + + def list_todo(self): + todo_lists = self.db.query(Todo).all() + if todo_lists: + return todo_lists + else: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo item not found") + + + def create_my_todo(self,title:str,description:str,published:bool): + todo = Todo( title=title, description=description,published=published) + if not todo: + self.db.add(todo) + self.db.commit() + if todo: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="this todo already exists") + + return todo + + + def update_my_todo(self, todo_id: int,title:str,description:str,published:bool): + todo = self.db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="Todo item not found") + if todo: + self.db.query(Todo).filter(Todo.id == todo_id).update({"title": title, "description": description, "published": published}) + self.db.commit() + + return todo + + + def delete_my_todo(self,todo_id: int): + todo = self.db.query(Todo).filter(Todo.id == todo_id).first() + if not todo: + raise HTTPException(status_code=404, detail="Todo item not found") + if todo: + self.db.query(Todo).filter(Todo.id == todo_id).delete() + self.db.commit() + + return todo \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py new file mode 100644 index 0000000..4b84331 --- /dev/null +++ b/app/schemas/todo.py @@ -0,0 +1,27 @@ +from pydantic import BaseModel, Field, validator +from typing import Optional +from datetime import datetime + + +class TodoRequest(BaseModel): + title: str = Field(..., min_length=3, max_length=100) + description: Optional[str] = Field(None, max_length=500) + published: Optional[bool] = Field(default=True) + + @validator('title') + def no_empty_title(cls, v): + if not v.strip(): + raise ValueError('Title cannot be empty') + return v + + + +class TodoResponse(BaseModel): + id: int + title: str + description: Optional[str] = None + published: bool + created_at: datetime + + class Config: + orm_mode = True \ No newline at end of file diff --git a/app/services/company_service.py b/app/services/company_service.py index 37e2476..1bdf26a 100644 --- a/app/services/company_service.py +++ b/app/services/company_service.py @@ -2,8 +2,6 @@ from sqlalchemy.orm import Session from app.repositories.company_repository import CompanyRepository - - class CompanyService: def __init__(self, db: Session): self.repo = CompanyRepository(db) diff --git a/app/services/todo_service.py b/app/services/todo_service.py new file mode 100644 index 0000000..5a7c9a8 --- /dev/null +++ b/app/services/todo_service.py @@ -0,0 +1,23 @@ +from sqlalchemy.orm import Session +from app.repositories.todo_repository import TodoRepository + + +class TodoService: + def __init__(self, db): + self.repo = TodoRepository(db) + + def get_all_todo(self): + todo_lists = self.repo.list_todo() + return todo_lists + + def create_todo(self,title:str,description:str,published:bool): + created = self.repo.create_my_todo(title,description,published) + return created + + def update_product(self, todo_id:int,title:str,description:str,published:bool): + updated = self.repo.update_my_todo(todo_id,title,description,published) + return updated + + def delete_product(self,todo_id): + deleted = self.repo.delete_my_todo(todo_id) + return deleted \ No newline at end of file From c6652c3bcf04b58de8d8012b004254af758f189a Mon Sep 17 00:00:00 2001 From: abubakar Date: Tue, 21 Oct 2025 18:27:46 +0500 Subject: [PATCH 09/10] implement query param like pagination and fix logic error --- .../34d17ef633a7_add_user_id_to_todos.py | 34 ++++++++++ app/controllers/auth_controller.py | 2 +- app/controllers/company_controller.py | 4 +- app/controllers/product_controller.py | 8 +-- app/controllers/todo_controller.py | 17 ++--- app/main.py | 12 ++-- app/models/todo.py | 7 +- app/models/user.py | 3 +- app/repositories/company_repository.py | 64 ++++--------------- app/repositories/product_repository.py | 51 ++++++--------- app/repositories/todo_repository.py | 50 +++++---------- app/repositories/user_repository.py | 36 ++++------- app/schemas/todo.py | 2 +- app/schemas/user.py | 2 +- app/services/company_service.py | 40 ++++++++---- app/services/product_service.py | 51 +++++++++++---- app/services/todo_service.py | 48 ++++++++++---- app/services/user_service.py | 32 +++++++--- 18 files changed, 255 insertions(+), 208 deletions(-) create mode 100644 alembic/versions/34d17ef633a7_add_user_id_to_todos.py diff --git a/alembic/versions/34d17ef633a7_add_user_id_to_todos.py b/alembic/versions/34d17ef633a7_add_user_id_to_todos.py new file mode 100644 index 0000000..c36ea11 --- /dev/null +++ b/alembic/versions/34d17ef633a7_add_user_id_to_todos.py @@ -0,0 +1,34 @@ +"""add user_id to todos + +Revision ID: 34d17ef633a7 +Revises: bdd3c45ac166 +Create Date: 2025-10-21 18:16:28.695532 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '34d17ef633a7' +down_revision: Union[str, Sequence[str], None] = 'bdd3c45ac166' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('todos', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'todos', 'user', ['user_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'todos', type_='foreignkey') + op.drop_column('todos', 'user_id') + # ### end Alembic commands ### diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py index 97d4af1..508f681 100644 --- a/app/controllers/auth_controller.py +++ b/app/controllers/auth_controller.py @@ -38,7 +38,7 @@ def me( service = UserService(db) return service.get_user(current_user.id) -@router.delete("/delete",dependencies=[Depends(get_current_user)]) +@router.delete("/delete") def delete( db: Session = Depends(get_db), current_user=Depends(get_current_user)): service = UserService(db) return service.delete_user(current_user.id) diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py index 3dafc3f..cf5d5a1 100644 --- a/app/controllers/company_controller.py +++ b/app/controllers/company_controller.py @@ -27,7 +27,7 @@ def get_my_company( return service.get_company(current_user.id) -@router.put("/me", response_model=CompanyResponse) +@router.put("/me") def edit_my_company( company: CompanyCreate, db: Session = Depends(get_db), @@ -37,7 +37,7 @@ def edit_my_company( return service.edit_company(current_user.id, company.name, company.company_type, company.location) -@router.delete("/me", dependencies=[Depends(get_current_user)]) +@router.delete("/me") def delete_my_company( db: Session = Depends(get_db), current_user=Depends(get_current_user) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py index a8d092a..083f181 100644 --- a/app/controllers/product_controller.py +++ b/app/controllers/product_controller.py @@ -28,11 +28,12 @@ def create_product( @router.get("/", response_model=list[ProductResponse]) def list_product( + skip: int = 0, limit: int = 10, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): product_service = ProductService(db) - return product_service.list_products() + return product_service.list_products(skip,limit) @router.get("/{product_id}", response_model=ProductResponse) @@ -44,8 +45,7 @@ def get_product_by_id( product_service = ProductService(db) return product_service.get_product(product_id) - -@router.put("/{product_id}", response_model=ProductResponse) +@router.put("/{product_id}") def update_product_by_id( product_id: int, product: ProductCreate, @@ -54,7 +54,7 @@ def update_product_by_id( product_service = ProductService(db) return product_service.update_product(product_id,product.name,product.price,product.description) -@router.delete("/{product_id}", response_model=ProductResponse) +@router.delete("/{product_id}") def delete_product_by_id( product_id: int, db: Session = Depends(get_db), diff --git a/app/controllers/todo_controller.py b/app/controllers/todo_controller.py index aad0637..b830599 100644 --- a/app/controllers/todo_controller.py +++ b/app/controllers/todo_controller.py @@ -11,11 +11,12 @@ @router.get("/", response_model=List[TodoResponse]) def get_all( + skip: int = 0, limit: int = 10, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): todo_service = TodoService(db) - return todo_service.get_all_todo() + return todo_service.get_all_todo(current_user.id,skip,limit) @router.post("/", response_model=TodoResponse) @@ -24,25 +25,25 @@ def create(todo : TodoRequest, current_user=Depends(get_current_user)): todo_service = TodoService(db) - return todo_service.create_todo(todo.title,todo.description,todo.published) + return todo_service.create_todo(todo.title,todo.description,todo.published,current_user.id) -@router.put("/{todo_id}", response_model=TodoResponse) +@router.put("/{todo_id}") def update(todo_id:int, todo : TodoRequest, db: Session = Depends(get_db), - current_user=Depends(get_current_user)): + ): todo_service = TodoService(db) - return todo_service.update_product(todo_id,todo.title,todo.description,todo.published) + return todo_service.update_todo(todo_id,todo.title,todo.description,todo.published) -@router.delete("/{todo_id}", response_model=TodoResponse) +@router.delete("/{todo_id}") def delete(todo_id:int, db: Session = Depends(get_db), - current_user=Depends(get_current_user)): + ): todo_service = TodoService(db) - return todo_service.delete_product(todo_id) \ No newline at end of file + return todo_service.delete_todo(todo_id) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 6f199d4..87d5c13 100644 --- a/app/main.py +++ b/app/main.py @@ -4,16 +4,16 @@ version = "v1" app = FastAPI(title="Company & Product API", - description="in which user create their company", version=version ) -app.middleware("")(add_process_time_header) +app.middleware("http")(add_process_time_header) + app.include_router(auth_controller.router, - prefix=f"/api/{version}/auth", tags=["Auth"]) + prefix=f"/api/{version}/auths", tags=["Auth"]) app.include_router(company_controller.router, - prefix=f"/api/{version}/company", tags=["Company"]) + prefix=f"/api/{version}/companies", tags=["Company"]) app.include_router(product_controller.router, - prefix=f"/api/{version}/product", tags=["Product"]) + prefix=f"/api/{version}/products", tags=["Product"]) app.include_router(todo_controller.router, - prefix=f"/api/{version}/todo", tags=["Todo"]) + prefix=f"/api/{version}/todos", tags=["Todo"]) diff --git a/app/models/todo.py b/app/models/todo.py index afbbe2b..885e51e 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -1,4 +1,6 @@ -from sqlalchemy import Column, Integer, String, Boolean, DateTime, func +from sqlalchemy import Column, Integer, String, Boolean, DateTime, func, ForeignKey +from sqlalchemy.orm import relationship + from app.database import Base class Todo(Base): @@ -9,6 +11,9 @@ class Todo(Base): description = Column(String(500), nullable=True) published = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) + user_id = Column(Integer, ForeignKey("user.id")) + + user = relationship("User", back_populates="todos") diff --git a/app/models/user.py b/app/models/user.py index 0033b39..af88433 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -10,4 +10,5 @@ class User(Base): username = Column(String, unique=True) password = Column(String) - company= relationship("Company",back_populates="user", uselist=False) \ No newline at end of file + company= relationship("Company",back_populates="user", uselist=False) + todos = relationship("Todo", back_populates="user") \ No newline at end of file diff --git a/app/repositories/company_repository.py b/app/repositories/company_repository.py index 6ba133a..e6927af 100644 --- a/app/repositories/company_repository.py +++ b/app/repositories/company_repository.py @@ -1,62 +1,26 @@ -from fastapi import HTTPException,status +from fastapi import HTTPException, status from sqlalchemy.orm import Session from app.models.company import Company class CompanyRepository: - def __init__(self ,db: Session): + def __init__(self, db: Session): self.db = db + def get_company_by_user_id(self, user_id: int): + return self.db.query(Company).filter(Company.user_id == user_id).first() - - def create_my_company(self, user_id: int, name: str, company_type: str, location: str): - existed=self.db.query(Company).filter(Company.user_id == user_id).first() - company = Company(user_id=user_id, name=name, company_type=company_type, location=location) - if existed: - raise HTTPException(status_code=400, detail="Company already exists") - if not existed: - self.db.add(company) - self.db.commit() + def create(self, company: Company): + self.db.add(company) + self.db.commit() + self.db.refresh(company) return company - - - - def get_my_company(self, user_id: int): - company = self.db.query(Company).filter(Company.user_id == user_id).first() - if not company: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="company not found" - ) + def update(self, company: Company): + self.db.commit() + self.db.refresh(company) return company - - - def edit_my_company(self, user_id: int,name: str, company_type: str, location: str ): - existed = self.db.query(Company).filter(Company.user_id == user_id).first() - if not existed: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="company not found" - ) - if existed: - self.db.query(Company).filter(Company.user_id == user_id).update( - {"name": name, "company_type": company_type, "location": location} - ) - self.db.commit() - - return existed - - - def delete_my_company(self, user_id: int): - existed = self.db.query(Company).filter(Company.user_id == user_id).first() - if not existed: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="company not found" - ) - if existed: - self.db.query(Company).filter(Company.user_id == user_id).delete() - self.db.commit() - - return existed - - + def delete(self, company: Company): + self.db.delete(company) + self.db.commit() diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py index 0d63a04..794460f 100644 --- a/app/repositories/product_repository.py +++ b/app/repositories/product_repository.py @@ -1,4 +1,3 @@ -from fastapi import HTTPException from sqlalchemy.orm import Session from app.models.product import Product @@ -6,40 +5,30 @@ class ProductRepository: def __init__(self, db: Session): self.db = db - def create_my_product(self,company_id:int, name: str, price: float, description: str) -> Product: - product = Product(company_id=company_id, name=name, price=price, description=description) - self.db.add(product) - self.db.commit() + def get_all(self, skip: int, limit: int): + return self.db.query(Product).offset(skip).limit(limit).all() - return product + def get_by_id(self, product_id: int): + return self.db.query(Product).filter(Product.id == product_id).first() - def list_my_product(self): - products = self.db.query(Product).all() - return products + def get_by_name_and_company(self, company_id: int, name: str): + return ( + self.db.query(Product) + .filter(Product.company_id == company_id, Product.name == name) + .first() + ) - def get_product_by_id(self, product_id: int): - product= self.db.query(Product).filter(Product.id == product_id).first() - if not product: - raise HTTPException(status_code=404, detail="Product not found") + def create(self, product: Product): + self.db.add(product) + self.db.commit() + self.db.refresh(product) return product - def update_my_product(self, product_id: int, name: str, price: float, description: str): - product = self.get_product_by_id(product_id) - if not product: - raise HTTPException(status_code=404, detail="Product not found") - if product: - self.db.query(Product).filter(Product.id == product_id).update({"name": name, "price": price, "description": description}) - self.db.commit() - + def update(self, product: Product): + self.db.commit() + self.db.refresh(product) return product - - def delete_my_product(self, product_id: int): - product = self.get_product_by_id(product_id) - if not product: - raise HTTPException(status_code=404, detail="Product not found") - if product: - self.db.query(Product).filter(Product.id == product_id).delete() - self.db.commit() - - return product \ No newline at end of file + def delete(self, product: Product): + self.db.delete(product) + self.db.commit() diff --git a/app/repositories/todo_repository.py b/app/repositories/todo_repository.py index d158f8c..8d3a2b3 100644 --- a/app/repositories/todo_repository.py +++ b/app/repositories/todo_repository.py @@ -1,48 +1,30 @@ -from fastapi import HTTPException, status from sqlalchemy.orm import Session from app.models.todo import Todo - class TodoRepository: def __init__(self, db: Session): self.db = db - def list_todo(self): - todo_lists = self.db.query(Todo).all() - if todo_lists: - return todo_lists - else: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo item not found") + def list_todo(self, user_id: int, skip: int, limit: int): + return self.db.query(Todo).filter(Todo.user_id == user_id).offset(skip).limit(limit).all() + def get_todo_by_title(self, title: str): + return self.db.query(Todo).filter(Todo.title == title).first() - def create_my_todo(self,title:str,description:str,published:bool): - todo = Todo( title=title, description=description,published=published) - if not todo: - self.db.add(todo) - self.db.commit() - if todo: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="this todo already exists") + def get_todo_by_id(self, todo_id: int): + return self.db.query(Todo).filter(Todo.id == todo_id).first() + def create(self, todo: Todo): + self.db.add(todo) + self.db.commit() + self.db.refresh(todo) return todo - - def update_my_todo(self, todo_id: int,title:str,description:str,published:bool): - todo = self.db.query(Todo).filter(Todo.id == todo_id).first() - if not todo: - raise HTTPException(status_code=404, detail="Todo item not found") - if todo: - self.db.query(Todo).filter(Todo.id == todo_id).update({"title": title, "description": description, "published": published}) - self.db.commit() - + def update(self, todo: Todo): + self.db.commit() + self.db.refresh(todo) return todo - - def delete_my_todo(self,todo_id: int): - todo = self.db.query(Todo).filter(Todo.id == todo_id).first() - if not todo: - raise HTTPException(status_code=404, detail="Todo item not found") - if todo: - self.db.query(Todo).filter(Todo.id == todo_id).delete() - self.db.commit() - - return todo \ No newline at end of file + def delete(self, todo: Todo): + self.db.delete(todo) + self.db.commit() diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py index 0f8ec8c..d5c3a6e 100644 --- a/app/repositories/user_repository.py +++ b/app/repositories/user_repository.py @@ -1,32 +1,24 @@ from sqlalchemy.orm import Session from app.models.user import User -from fastapi import HTTPException,status + class UserRepository: def __init__(self, db: Session): self.db = db + def get_by_id(self, user_id: int): + return self.db.query(User).filter(User.id == user_id).first() - def get_by_me(self, user_id: int): - user = self.db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="User not found") - return user - - def create_user(self, username: str, hashed_pw: str): - user = self.db.query(User).filter(User.username == username).first() - if not user: - user = User(username=username, password=hashed_pw) - self.db.add(user) - self.db.commit() + def get_by_username(self, username: str): + return self.db.query(User).filter(User.username == username).first() - return user + def create(self, username: str, hashed_pw: str): + new_user = User(username=username, password=hashed_pw) + self.db.add(new_user) + self.db.commit() + self.db.refresh(new_user) + return new_user - def delete_user(self, user_id: int): - user = self.db.query(User).filter(User.id == user_id).first() - if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="User not found") - if user: - self.db.delete(user) - self.db.commit() - return {"message": "User deleted successfully"} \ No newline at end of file + def delete(self, user: User): + self.db.delete(user) + self.db.commit() diff --git a/app/schemas/todo.py b/app/schemas/todo.py index 4b84331..a091ad1 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -24,4 +24,4 @@ class TodoResponse(BaseModel): created_at: datetime class Config: - orm_mode = True \ No newline at end of file + from_attributes = True \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index f72d10b..b736c19 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -2,7 +2,7 @@ class UserCreate(BaseModel): - username: str = Field(..., min_length=3, max_length=15, description="User's unique name") + username: str = Field(..., min_length=2, max_length=15, description="User's unique name") password: str diff --git a/app/services/company_service.py b/app/services/company_service.py index 1bdf26a..04303eb 100644 --- a/app/services/company_service.py +++ b/app/services/company_service.py @@ -1,26 +1,44 @@ from fastapi import HTTPException, status from sqlalchemy.orm import Session +from app.models.company import Company from app.repositories.company_repository import CompanyRepository + class CompanyService: def __init__(self, db: Session): - self.repo = CompanyRepository(db) + self.repo = CompanyRepository(db) - def create_company(self, user_id: int,name: str, company_type: str, location: str): + def create_company(self, user_id: int, name: str, company_type: str, location: str): + existing = self.repo.get_company_by_user_id(user_id) + if existing: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Company already exists") - existed = self.repo.create_my_company(user_id, name, company_type, location) - if not existed: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") - return existed + company = Company(user_id=user_id, name=name, company_type=company_type, location=location) + created = self.repo.create(company) + return {"message": "Company created successfully", "data": created} def get_company(self, user_id: int): - company = self.repo.get_my_company(user_id) + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") return company def edit_company(self, user_id: int, name: str, company_type: str, location: str): - updated =self.repo.edit_my_company(user_id, name, company_type, location) - return updated + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + + company.name = name + company.company_type = company_type + company.location = location + + self.repo.update(company) + return {"message": "Company updated successfully"} def delete_company(self, user_id: int): - deleted = self.repo.delete_my_company(user_id) - return deleted + company = self.repo.get_company_by_user_id(user_id) + if not company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") + + self.repo.delete(company) + return {"message": "Company deleted successfully"} diff --git a/app/services/product_service.py b/app/services/product_service.py index 8ae43c4..deda86c 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -1,27 +1,52 @@ +from fastapi import HTTPException, status from sqlalchemy.orm import Session +from app.models.product import Product from app.repositories.product_repository import ProductRepository - - class ProductService: def __init__(self, db: Session): self.repo = ProductRepository(db) - def create_product(self, company_id: int,name: str,price: float,description: str): - created = self.repo.create_my_product(company_id,name,price,description) - return created - - def list_products(self): - products = self.repo.list_my_product() + def create_product(self, company_id: int, name: str, price: float, description: str): + existing_product = self.repo.get_by_name_and_company(company_id, name) + if existing_product: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="This product already exists for the company" + ) + + product = Product(company_id=company_id, name=name, price=price, description=description) + created_product = self.repo.create(product) + return created_product + + def list_products(self, skip: int, limit: int): + products = self.repo.get_all(skip, limit) + if not products: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No products found") return products def get_product(self, product_id: int): - product = self.repo.get_product_by_id(product_id) + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") return product - def update_product(self, product_id: int, name: str,price: float,description: str): - product = self.repo.update_my_product(product_id,name,price,description) - return product + def update_product(self, product_id: int, name: str, price: float, description: str): + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + + product.name = name + product.price = price + product.description = description + + updated = self.repo.update(product) + return {"message": "Product updated successfully", "data": updated} def delete_product(self, product_id: int): - product = self.repo.delete_my_product(product_id) \ No newline at end of file + product = self.repo.get_by_id(product_id) + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") + + self.repo.delete(product) + return {"message": "Product deleted successfully"} diff --git a/app/services/todo_service.py b/app/services/todo_service.py index 5a7c9a8..05996e5 100644 --- a/app/services/todo_service.py +++ b/app/services/todo_service.py @@ -1,23 +1,43 @@ -from sqlalchemy.orm import Session +from fastapi import HTTPException, status +from app.models.todo import Todo from app.repositories.todo_repository import TodoRepository - class TodoService: def __init__(self, db): self.repo = TodoRepository(db) - def get_all_todo(self): - todo_lists = self.repo.list_todo() - return todo_lists + def get_all_todo(self,user_id:int,skip: int, limit: int): + todos = self.repo.list_todo(user_id,skip, limit) + if not todos: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No todos found") + + return todos + + def create_todo(self, title: str, description: str, published: bool, user_id: int): + existing = self.repo.get_todo_by_title(title) + if existing: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Todo already exists") + + todo = Todo(title=title, description=description, published=published, user_id=user_id) + create_todo = self.repo.create(todo) + return create_todo + + def update_todo(self, todo_id: int, title: str, description: str, published: bool): + todo = self.repo.get_todo_by_id(todo_id) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + + todo.title = title + todo.description = description + todo.published = published - def create_todo(self,title:str,description:str,published:bool): - created = self.repo.create_my_todo(title,description,published) - return created + updated = self.repo.update(todo) + return {"message": "Todo updated successfully", "data": updated} - def update_product(self, todo_id:int,title:str,description:str,published:bool): - updated = self.repo.update_my_todo(todo_id,title,description,published) - return updated + def delete_todo(self, todo_id: int): + todo = self.repo.get_todo_by_id(todo_id) + if not todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") - def delete_product(self,todo_id): - deleted = self.repo.delete_my_todo(todo_id) - return deleted \ No newline at end of file + self.repo.delete(todo) + return {"message": "Todo deleted successfully"} diff --git a/app/services/user_service.py b/app/services/user_service.py index bc5cdc1..50e4919 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -8,16 +8,32 @@ def __init__(self, db: Session): self.repo = UserRepository(db) def create_user(self, username: str, hashed_pw: str): - if self.repo.create_user(username, hashed_pw): - raise HTTPException(status_code=status.HTTP_201_CREATED, detail="User created successfully") + existing_user = self.repo.get_by_username(username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already exists" + ) - return self.repo.create_user(username, hashed_pw) + self.repo.create(username, hashed_pw) + return {"message": "User created successfully"} def get_user(self, user_id: int): - me = self.repo.get_by_me(user_id) - return me - + user = self.repo.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return user def delete_user(self, user_id: int): - me = self.repo.delete_user(user_id) - return me + user = self.repo.get_by_id(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + self.repo.delete(user) + return {"message": "User deleted successfully"} From 5972120e9d0354319e18160351c464b96323cc9f Mon Sep 17 00:00:00 2001 From: abubakar Date: Fri, 24 Oct 2025 16:59:17 +0500 Subject: [PATCH 10/10] changing in company and todo CRUD --- .gitignore | 2 + .../998c60089b82_renamed_todos_to_todo.py | 32 ++++++++++ ...hanged_company_type_nullable_false_and_.py | 62 +++++++++++++++++++ ...a6411_add_full_name_bio_profile_to_user.py | 36 +++++++++++ app/auth.py | 7 +-- app/controllers/auth_controller.py | 26 ++++++-- app/controllers/company_controller.py | 14 +++-- app/controllers/product_controller.py | 24 ++++--- app/controllers/profile_controller.py | 18 ++++++ app/controllers/todo_controller.py | 26 ++++---- app/main.py | 7 ++- app/models/company.py | 2 +- app/models/todo.py | 10 +-- app/models/user.py | 5 +- app/repositories/company_repository.py | 23 ++++--- app/repositories/product_repository.py | 21 +++++-- app/repositories/profile_repository.py | 10 +++ app/repositories/todo_repository.py | 26 +++++--- app/repositories/user_repository.py | 10 +++ app/schemas/company.py | 13 ++-- app/schemas/product.py | 8 ++- app/schemas/todo.py | 7 ++- app/schemas/user.py | 30 +++++++-- app/services/company_service.py | 18 ++---- app/services/product_service.py | 16 ++--- app/services/profile_service.py | 15 +++++ app/services/todo_service.py | 25 +++----- app/services/user_service.py | 15 +++-- 28 files changed, 379 insertions(+), 129 deletions(-) create mode 100644 alembic/versions/998c60089b82_renamed_todos_to_todo.py create mode 100644 alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py create mode 100644 alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py create mode 100644 app/controllers/profile_controller.py create mode 100644 app/repositories/profile_repository.py create mode 100644 app/services/profile_service.py diff --git a/.gitignore b/.gitignore index 3ccd43f..fb3dde7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env +config.py # Ignore Python cache files __pycache__/ *.pyc diff --git a/alembic/versions/998c60089b82_renamed_todos_to_todo.py b/alembic/versions/998c60089b82_renamed_todos_to_todo.py new file mode 100644 index 0000000..03df691 --- /dev/null +++ b/alembic/versions/998c60089b82_renamed_todos_to_todo.py @@ -0,0 +1,32 @@ +"""renamed todos to todo + +Revision ID: 998c60089b82 +Revises: b084aab31149 +Create Date: 2025-10-23 14:25:23.896922 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '998c60089b82' +down_revision: Union[str, Sequence[str], None] = 'b084aab31149' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.rename_table('todos', 'todo') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.rename_table('todo', 'todos') + # ### end Alembic commands ### diff --git a/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py b/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py new file mode 100644 index 0000000..80f95f3 --- /dev/null +++ b/alembic/versions/b084aab31149_changed_company_type_nullable_false_and_.py @@ -0,0 +1,62 @@ +"""Changed company_type nullable false and renamed todos to todo + +Revision ID: b084aab31149 +Revises: b688ad9a6411 +Create Date: 2025-10-22 16:53:29.419183 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'b084aab31149' +down_revision: Union[str, Sequence[str], None] = 'b688ad9a6411' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('todo', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=100), nullable=False), + sa.Column('description', sa.String(length=500), nullable=True), + sa.Column('published', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_todo_id'), 'todo', ['id'], unique=False) + op.drop_index(op.f('ix_todos_id'), table_name='todos') + op.drop_table('todos') + op.alter_column('company', 'company_type', + existing_type=sa.VARCHAR(), + nullable=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('company', 'company_type', + existing_type=sa.VARCHAR(), + nullable=True) + op.create_table('todos', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('title', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('description', sa.VARCHAR(length=500), autoincrement=False, nullable=True), + sa.Column('published', sa.BOOLEAN(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=True), + sa.Column('user_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], name=op.f('todos_user_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('todos_pkey')) + ) + op.create_index(op.f('ix_todos_id'), 'todos', ['id'], unique=False) + op.drop_index(op.f('ix_todo_id'), table_name='todo') + op.drop_table('todo') + # ### end Alembic commands ### diff --git a/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py b/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py new file mode 100644 index 0000000..ea836ac --- /dev/null +++ b/alembic/versions/b688ad9a6411_add_full_name_bio_profile_to_user.py @@ -0,0 +1,36 @@ +"""Add full_name bio profile to user + +Revision ID: b688ad9a6411 +Revises: 34d17ef633a7 +Create Date: 2025-10-22 15:35:30.465282 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b688ad9a6411' +down_revision: Union[str, Sequence[str], None] = '34d17ef633a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('user', sa.Column('full_name', sa.String(), nullable=True)) + op.add_column('user', sa.Column('bio', sa.String(), nullable=True)) + op.add_column('user', sa.Column('profile', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('user', 'profile') + op.drop_column('user', 'bio') + op.drop_column('user', 'full_name') + # ### end Alembic commands ### diff --git a/app/auth.py b/app/auth.py index d48bd3c..8e89768 100644 --- a/app/auth.py +++ b/app/auth.py @@ -8,10 +8,7 @@ from app.database import get_db from fastapi.security import HTTPAuthorizationCredentials - -SECRET_KEY = "af3287c8391bb9f4f7a72feb3b85f72e1d5bd07cbf4fa4ad9497c78412923312" -ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 7 * 24 * 60 +from config import ACCESS_TOKEN_EXPIRE_DAYS, ALGORITHM, SECRET_KEY pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") bearer_scheme = HTTPBearer() @@ -28,7 +25,7 @@ def get_password_hash(password): # ---------------- TOKEN CREATION ---------------- # def create_access_token(user_id: int): - expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + expire = datetime.utcnow() + timedelta(days=ACCESS_TOKEN_EXPIRE_DAYS) to_encode = {"sub": str(user_id), "exp": expire} return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) diff --git a/app/controllers/auth_controller.py b/app/controllers/auth_controller.py index 508f681..d8a85ba 100644 --- a/app/controllers/auth_controller.py +++ b/app/controllers/auth_controller.py @@ -1,15 +1,18 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from app.schemas.user import UserCreate, UserResponse +from app.schemas.user import UserCreateRequest, UserResponse, UserLoginRequest, UserProfileResponse, UserUpdateRequest, \ + MessageResponse from app.services.user_service import UserService from app.database import get_db from app.auth import create_access_token, verify_password, get_password_hash, get_current_user from app.models.user import User +from config import config_reader + router = APIRouter() @router.post("/register", response_model=UserResponse) -def register(user: UserCreate, db: Session = Depends(get_db)): +def register(user: UserCreateRequest, db: Session = Depends(get_db)): service = UserService(db) hashed_pw= get_password_hash(user.password) try: @@ -20,8 +23,8 @@ def register(user: UserCreate, db: Session = Depends(get_db)): @router.post("/token") -def login(user: UserCreate, db: Session = Depends(get_db)): - # Query using database model +def login(user: UserLoginRequest, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.username == user.username).first() if not db_user or not verify_password(user.password, db_user.password): @@ -38,7 +41,18 @@ def me( service = UserService(db) return service.get_user(current_user.id) -@router.delete("/delete") +@router.put("/update", response_model=UserProfileResponse) +def update( + user:UserUpdateRequest, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + service = UserService(db) + return service.updated_user(current_user.id, user.username,user.full_name, user.bio,user.profile) + + +@router.delete("/delete",response_model=MessageResponse) def delete( db: Session = Depends(get_db), current_user=Depends(get_current_user)): service = UserService(db) - return service.delete_user(current_user.id) + deleted_user = service.delete_user(current_user.id) + return MessageResponse(message=config_reader.get_value("USER_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/app/controllers/company_controller.py b/app/controllers/company_controller.py index cf5d5a1..635e0fa 100644 --- a/app/controllers/company_controller.py +++ b/app/controllers/company_controller.py @@ -1,16 +1,17 @@ from fastapi import APIRouter, Depends from sqlalchemy.orm import Session -from app.schemas.company import CompanyCreate, CompanyResponse +from app.schemas.company import CompanyCreateRequest, CompanyResponse, MessageResponse from app.services.company_service import CompanyService from app.database import get_db from app.auth import get_current_user +from config import config_reader router = APIRouter() @router.post("/", response_model=CompanyResponse) def create_company( - company: CompanyCreate, + company: CompanyCreateRequest, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): @@ -27,9 +28,9 @@ def get_my_company( return service.get_company(current_user.id) -@router.put("/me") +@router.put("/update-me",response_model=CompanyResponse) def edit_my_company( - company: CompanyCreate, + company: CompanyCreateRequest, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): @@ -37,10 +38,11 @@ def edit_my_company( return service.edit_company(current_user.id, company.name, company.company_type, company.location) -@router.delete("/me") +@router.delete("/delete-me",response_model=MessageResponse) def delete_my_company( db: Session = Depends(get_db), current_user=Depends(get_current_user) ): service = CompanyService(db) - return service.delete_company(current_user.id) + service.delete_company(current_user.id) + return MessageResponse(message=config_reader.get_value("COMPANY_DELETE_MESSAGE")) diff --git a/app/controllers/product_controller.py b/app/controllers/product_controller.py index 083f181..b890db1 100644 --- a/app/controllers/product_controller.py +++ b/app/controllers/product_controller.py @@ -1,18 +1,18 @@ from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from app.schemas.product import ProductCreate, ProductResponse +from app.schemas.product import ProductCreateRequest, ProductResponse, MessageResponse from app.services.product_service import ProductService from app.services.company_service import CompanyService from app.database import get_db from app.auth import get_current_user - +from config import config_reader router = APIRouter() @router.post("/", response_model=ProductResponse) def create_product( - product: ProductCreate, + product: ProductCreateRequest, db: Session = Depends(get_db), current_user=Depends(get_current_user) ): @@ -30,35 +30,33 @@ def create_product( def list_product( skip: int = 0, limit: int = 10, db: Session = Depends(get_db), - current_user=Depends(get_current_user) + ): product_service = ProductService(db) return product_service.list_products(skip,limit) -@router.get("/{product_id}", response_model=ProductResponse) +@router.get("/{product_id}", response_model=ProductResponse,dependencies=[Depends(get_current_user)]) def get_product_by_id( product_id: int, db: Session = Depends(get_db), - current_user=Depends(get_current_user) ): product_service = ProductService(db) return product_service.get_product(product_id) -@router.put("/{product_id}") +@router.put("/{product_id}", response_model=ProductResponse,dependencies=[Depends(get_current_user)]) def update_product_by_id( product_id: int, - product: ProductCreate, - db: Session = Depends(get_db), - current_user=Depends(get_current_user)): + product: ProductCreateRequest, + db: Session = Depends(get_db),): product_service = ProductService(db) return product_service.update_product(product_id,product.name,product.price,product.description) -@router.delete("/{product_id}") +@router.delete("/{product_id}",dependencies=[Depends(get_current_user)],response_model=MessageResponse) def delete_product_by_id( product_id: int, db: Session = Depends(get_db), -current_user=Depends(get_current_user) ): product_service = ProductService(db) - return product_service.delete_product(product_id) \ No newline at end of file + product_service.delete_product(product_id) + return MessageResponse(message=config_reader.get_value("PRODUCT_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/app/controllers/profile_controller.py b/app/controllers/profile_controller.py new file mode 100644 index 0000000..aae2921 --- /dev/null +++ b/app/controllers/profile_controller.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session +from app.database import get_db +from app.auth import get_current_user +from app.services.profile_service import ProfileService + +from app.schemas.user import UserProfileResponse + +router = APIRouter() + + +@router.get("/",response_model=UserProfileResponse) +def get_profile( + db: Session = Depends(get_db), + current_user=Depends(get_current_user) +): + service = ProfileService(db) + return service.get_profile(current_user.id) diff --git a/app/controllers/todo_controller.py b/app/controllers/todo_controller.py index b830599..2c0bd61 100644 --- a/app/controllers/todo_controller.py +++ b/app/controllers/todo_controller.py @@ -1,11 +1,12 @@ from typing import List -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends from sqlalchemy.orm import Session from app.auth import get_current_user from app.database import get_db -from app.schemas.todo import TodoResponse,TodoRequest +from app.schemas.todo import TodoResponse, TodoRequest, MessageResponse from app.services.todo_service import TodoService +from config import config_reader router = APIRouter() @@ -13,26 +14,26 @@ def get_all( skip: int = 0, limit: int = 10, db: Session = Depends(get_db), - current_user=Depends(get_current_user) + ): todo_service = TodoService(db) - return todo_service.get_all_todo(current_user.id,skip,limit) + return todo_service.get_all_todo(skip,limit) -@router.post("/", response_model=TodoResponse) +@router.post("/", response_model=TodoResponse,dependencies=[Depends(get_current_user)]) def create(todo : TodoRequest, db: Session = Depends(get_db), - current_user=Depends(get_current_user)): + ): todo_service = TodoService(db) - return todo_service.create_todo(todo.title,todo.description,todo.published,current_user.id) + return todo_service.create_todo(todo.title,todo.description,todo.published) -@router.put("/{todo_id}") +@router.put("/{todo_id}",response_model=TodoResponse,dependencies=[Depends(get_current_user)]) def update(todo_id:int, todo : TodoRequest, - db: Session = Depends(get_db), + db: Session = Depends(get_db), ): todo_service = TodoService(db) @@ -40,10 +41,11 @@ def update(todo_id:int, -@router.delete("/{todo_id}") +@router.delete("/{todo_id}",dependencies=[Depends(get_current_user)],response_model=MessageResponse) def delete(todo_id:int, - db: Session = Depends(get_db), + db: Session = Depends(get_db), ): todo_service = TodoService(db) - return todo_service.delete_todo(todo_id) \ No newline at end of file + todo_service.delete_todo(todo_id) + return MessageResponse(message=config_reader.get_value("ITEM_DELETED_SUCCESSFULLY")) \ No newline at end of file diff --git a/app/main.py b/app/main.py index 87d5c13..7a4cebd 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ from fastapi import FastAPI -from app.controllers import auth_controller, company_controller, product_controller, todo_controller +from app.controllers import auth_controller, company_controller, product_controller, todo_controller, profile_controller from app.middlewares.time_middleware import add_process_time_header version = "v1" @@ -9,6 +9,9 @@ app.middleware("http")(add_process_time_header) + +app.include_router(profile_controller.router, + prefix=f"/api/{version}/profiles", tags=["Profile"]) app.include_router(auth_controller.router, prefix=f"/api/{version}/auths", tags=["Auth"]) app.include_router(company_controller.router, @@ -17,3 +20,5 @@ prefix=f"/api/{version}/products", tags=["Product"]) app.include_router(todo_controller.router, prefix=f"/api/{version}/todos", tags=["Todo"]) + + diff --git a/app/models/company.py b/app/models/company.py index 2c4b50a..2362444 100644 --- a/app/models/company.py +++ b/app/models/company.py @@ -9,7 +9,7 @@ class Company(Base): id =Column(Integer, primary_key=True, index=True) name= Column(String) location = Column(String) - company_type = Column(String, nullable=True) + company_type = Column(String, nullable=False) user_id = Column(Integer, ForeignKey("user.id")) diff --git a/app/models/todo.py b/app/models/todo.py index 885e51e..1b8e1c6 100644 --- a/app/models/todo.py +++ b/app/models/todo.py @@ -1,19 +1,19 @@ -from sqlalchemy import Column, Integer, String, Boolean, DateTime, func, ForeignKey -from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, Boolean, DateTime, func + from app.database import Base class Todo(Base): - __tablename__ = "todos" + __tablename__ = "todo" id = Column(Integer, primary_key=True, index=True) title = Column(String(100), nullable=False) description = Column(String(500), nullable=True) published = Column(Boolean, default=True, nullable=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) - user_id = Column(Integer, ForeignKey("user.id")) - user = relationship("User", back_populates="todos") + + diff --git a/app/models/user.py b/app/models/user.py index af88433..f2376c2 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -9,6 +9,9 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) username = Column(String, unique=True) password = Column(String) + full_name = Column(String, nullable=True) + bio = Column(String, nullable=True) + profile = Column(String, nullable=True) company= relationship("Company",back_populates="user", uselist=False) - todos = relationship("Todo", back_populates="user") \ No newline at end of file + diff --git a/app/repositories/company_repository.py b/app/repositories/company_repository.py index e6927af..551efe1 100644 --- a/app/repositories/company_repository.py +++ b/app/repositories/company_repository.py @@ -10,17 +10,24 @@ def __init__(self, db: Session): def get_company_by_user_id(self, user_id: int): return self.db.query(Company).filter(Company.user_id == user_id).first() - def create(self, company: Company): - self.db.add(company) + def create(self, name:str, company_type:str, location:str): + created_company = Company(name=name, company_type=company_type, location=location) + self.db.add(created_company) self.db.commit() - self.db.refresh(company) - return company + return created_company + + def update(self,user_id:int,name: str, company_type: str, location: str): + updated_company = self.db.query(Company).filter(Company.user_id == user_id).first() + if not updated_company: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Company not found") + updated_company.name = name + updated_company.company_type = company_type + updated_company.location = location - def update(self, company: Company): self.db.commit() - self.db.refresh(company) - return company - def delete(self, company: Company): + return updated_company + + def delete(self, company:Company): self.db.delete(company) self.db.commit() diff --git a/app/repositories/product_repository.py b/app/repositories/product_repository.py index 794460f..c109568 100644 --- a/app/repositories/product_repository.py +++ b/app/repositories/product_repository.py @@ -1,3 +1,4 @@ +from fastapi import HTTPException,status from sqlalchemy.orm import Session from app.models.product import Product @@ -18,15 +19,23 @@ def get_by_name_and_company(self, company_id: int, name: str): .first() ) - def create(self, product: Product): - self.db.add(product) + def create(self,company_id:int,name:str,price:float,description:str): + created_product = Product(company_id=company_id,name=name,price=price,description=description) + self.db.add(created_product) self.db.commit() - self.db.refresh(product) - return product + self.db.refresh(created_product) + return created_product + + def update(self,product_id:int, name:str, price:float,description:str,): + product = self.db.query(Product).filter(Product.id == product_id).first() + + if not product: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="Product not found") + product.name = name + product.price = price + product.description = description - def update(self, product: Product): self.db.commit() - self.db.refresh(product) return product def delete(self, product: Product): diff --git a/app/repositories/profile_repository.py b/app/repositories/profile_repository.py new file mode 100644 index 0000000..ad93db1 --- /dev/null +++ b/app/repositories/profile_repository.py @@ -0,0 +1,10 @@ +from sqlalchemy.orm import Session +from app.models.user import User + +class ProfileRepository: + def __init__(self, db: Session): + self.db = db + + + def get_by_user_id(self, user_id: int): + return self.db.query(User).filter(User.id == user_id).first() diff --git a/app/repositories/todo_repository.py b/app/repositories/todo_repository.py index 8d3a2b3..7024554 100644 --- a/app/repositories/todo_repository.py +++ b/app/repositories/todo_repository.py @@ -1,12 +1,12 @@ from sqlalchemy.orm import Session from app.models.todo import Todo - +from fastapi import HTTPException, status class TodoRepository: def __init__(self, db: Session): self.db = db - def list_todo(self, user_id: int, skip: int, limit: int): - return self.db.query(Todo).filter(Todo.user_id == user_id).offset(skip).limit(limit).all() + def list_todo(self, skip: int, limit: int): + return self.db.query(Todo).offset(skip).limit(limit).all() def get_todo_by_title(self, title: str): return self.db.query(Todo).filter(Todo.title == title).first() @@ -14,16 +14,22 @@ def get_todo_by_title(self, title: str): def get_todo_by_id(self, todo_id: int): return self.db.query(Todo).filter(Todo.id == todo_id).first() - def create(self, todo: Todo): - self.db.add(todo) + def create(self, title:str,description:str,published:bool): + created_todo = Todo(title=title,description=description,published=published) + self.db.add(created_todo) self.db.commit() - self.db.refresh(todo) - return todo + return created_todo + + def update(self,todo_id:int,title:str,description:str,published:bool): + updated_todo = self.db.query(Todo).filter(Todo.id == todo_id).first() + if not updated_todo: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,detail="todo item not found") - def update(self, todo: Todo): + updated_todo.title = title + updated_todo.description = description + updated_todo.published = published self.db.commit() - self.db.refresh(todo) - return todo + return updated_todo def delete(self, todo: Todo): self.db.delete(todo) diff --git a/app/repositories/user_repository.py b/app/repositories/user_repository.py index d5c3a6e..65b02a1 100644 --- a/app/repositories/user_repository.py +++ b/app/repositories/user_repository.py @@ -19,6 +19,16 @@ def create(self, username: str, hashed_pw: str): self.db.refresh(new_user) return new_user + def update(self, user_id:int,username: str, full_name: str,bio:str, profile:str,): + updated_user= self.db.query(User).filter(User.id == user_id).first() + updated_user.username = username + updated_user.full_name = full_name + updated_user.bio = bio + updated_user.profile = profile + + self.db.commit() + return updated_user + def delete(self, user: User): self.db.delete(user) self.db.commit() diff --git a/app/schemas/company.py b/app/schemas/company.py index 34a7b77..f31cdb2 100644 --- a/app/schemas/company.py +++ b/app/schemas/company.py @@ -3,17 +3,18 @@ from app.schemas.product import ProductResponse -class CompanyCreate(BaseModel): +class CompanyCreateRequest(BaseModel): name: str location: str - company_type: Optional[str] = None + company_type: str class CompanyResponse(BaseModel): id: int name: str location: str - company_type: Optional[str] = None + company_type: str products: List[ProductResponse] = [] - # - # class Config: - # from_attributes = True + +class MessageResponse(BaseModel): + message: str + diff --git a/app/schemas/product.py b/app/schemas/product.py index e74b1b1..98be3d9 100644 --- a/app/schemas/product.py +++ b/app/schemas/product.py @@ -2,12 +2,13 @@ from pydantic import BaseModel,Field -class ProductCreate(BaseModel): +class ProductCreateRequest(BaseModel): name: str = Field(..., min_length=3, max_length=50, description="Product name") price: float = Field(..., gt=0, description="Price must be greater than 0") description: Optional[str] = Field(None, max_length=200) + class ProductResponse(BaseModel): id: int name: str @@ -16,3 +17,8 @@ class ProductResponse(BaseModel): # # class Config: # from_attributes = True + + + +class MessageResponse(BaseModel): + message: str \ No newline at end of file diff --git a/app/schemas/todo.py b/app/schemas/todo.py index a091ad1..cf69302 100644 --- a/app/schemas/todo.py +++ b/app/schemas/todo.py @@ -24,4 +24,9 @@ class TodoResponse(BaseModel): created_at: datetime class Config: - from_attributes = True \ No newline at end of file + from_attributes = True + + + +class MessageResponse(BaseModel): + message: str \ No newline at end of file diff --git a/app/schemas/user.py b/app/schemas/user.py index b736c19..ccad710 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,19 +1,39 @@ from pydantic import BaseModel,Field +from typing import Optional - -class UserCreate(BaseModel): +class UserCreateRequest(BaseModel): username: str = Field(..., min_length=2, max_length=15, description="User's unique name") password: str + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None + +class UserUpdateRequest(BaseModel): + username: str = Field(..., min_length=2, max_length=15, description="User's unique name") + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None class UserResponse(BaseModel): id: int username: str +class UserLoginRequest(BaseModel): + username: str + password: str - class Config: - from_attributes = True +class UserProfileResponse(BaseModel): + id: int + username: str + full_name: Optional[str] = None + bio: Optional[str] = None + profile: Optional[str] = None class TokenResponse(BaseModel): - access_token: str \ No newline at end of file + access_token: str + + +class MessageResponse(BaseModel): + message: str diff --git a/app/services/company_service.py b/app/services/company_service.py index 04303eb..5a8b3f8 100644 --- a/app/services/company_service.py +++ b/app/services/company_service.py @@ -1,6 +1,5 @@ from fastapi import HTTPException, status from sqlalchemy.orm import Session -from app.models.company import Company from app.repositories.company_repository import CompanyRepository @@ -13,9 +12,8 @@ def create_company(self, user_id: int, name: str, company_type: str, location: s if existing: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Company already exists") - company = Company(user_id=user_id, name=name, company_type=company_type, location=location) - created = self.repo.create(company) - return {"message": "Company created successfully", "data": created} + created = self.repo.create(name, company_type, location) + return created def get_company(self, user_id: int): company = self.repo.get_company_by_user_id(user_id) @@ -28,17 +26,13 @@ def edit_company(self, user_id: int, name: str, company_type: str, location: str if not company: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") - company.name = name - company.company_type = company_type - company.location = location - - self.repo.update(company) - return {"message": "Company updated successfully"} + updated = self.repo.update(user_id,name,company_type,location) + return updated def delete_company(self, user_id: int): company = self.repo.get_company_by_user_id(user_id) if not company: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Company not found") - self.repo.delete(company) - return {"message": "Company deleted successfully"} + deleted = self.repo.delete(company) + return deleted diff --git a/app/services/product_service.py b/app/services/product_service.py index deda86c..24d24bb 100644 --- a/app/services/product_service.py +++ b/app/services/product_service.py @@ -1,6 +1,5 @@ from fastapi import HTTPException, status from sqlalchemy.orm import Session -from app.models.product import Product from app.repositories.product_repository import ProductRepository class ProductService: @@ -15,8 +14,7 @@ def create_product(self, company_id: int, name: str, price: float, description: detail="This product already exists for the company" ) - product = Product(company_id=company_id, name=name, price=price, description=description) - created_product = self.repo.create(product) + created_product = self.repo.create(company_id,name,price,description) return created_product def list_products(self, skip: int, limit: int): @@ -36,17 +34,13 @@ def update_product(self, product_id: int, name: str, price: float, description: if not product: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") - product.name = name - product.price = price - product.description = description - - updated = self.repo.update(product) - return {"message": "Product updated successfully", "data": updated} + updated = self.repo.update(product_id,name, price, description) + return updated def delete_product(self, product_id: int): product = self.repo.get_by_id(product_id) if not product: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") - self.repo.delete(product) - return {"message": "Product deleted successfully"} + deleted = self.repo.delete(product) + return deleted diff --git a/app/services/profile_service.py b/app/services/profile_service.py new file mode 100644 index 0000000..e06e4e8 --- /dev/null +++ b/app/services/profile_service.py @@ -0,0 +1,15 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.repositories.profile_repository import ProfileRepository + +class ProfileService: + def __init__(self, db: Session): + self.repo = ProfileRepository(db) + + + def get_profile(self, user_id: int): + profile = self.repo.get_by_user_id(user_id) + if not profile: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Profile not found") + return profile diff --git a/app/services/todo_service.py b/app/services/todo_service.py index 05996e5..f8f3a11 100644 --- a/app/services/todo_service.py +++ b/app/services/todo_service.py @@ -1,25 +1,24 @@ from fastapi import HTTPException, status -from app.models.todo import Todo +from sqlalchemy.orm import Session from app.repositories.todo_repository import TodoRepository class TodoService: - def __init__(self, db): + def __init__(self, db: Session): self.repo = TodoRepository(db) - def get_all_todo(self,user_id:int,skip: int, limit: int): - todos = self.repo.list_todo(user_id,skip, limit) + def get_all_todo(self,skip: int, limit: int): + todos = self.repo.list_todo(skip, limit) if not todos: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No todos found") return todos - def create_todo(self, title: str, description: str, published: bool, user_id: int): + def create_todo(self, title: str, description: str, published: bool): existing = self.repo.get_todo_by_title(title) if existing: raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Todo already exists") - todo = Todo(title=title, description=description, published=published, user_id=user_id) - create_todo = self.repo.create(todo) + create_todo = self.repo.create(title,description,published) return create_todo def update_todo(self, todo_id: int, title: str, description: str, published: bool): @@ -27,17 +26,13 @@ def update_todo(self, todo_id: int, title: str, description: str, published: boo if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") - todo.title = title - todo.description = description - todo.published = published - - updated = self.repo.update(todo) - return {"message": "Todo updated successfully", "data": updated} + updated = self.repo.update(todo_id,title,description,published) + return updated def delete_todo(self, todo_id: int): todo = self.repo.get_todo_by_id(todo_id) if not todo: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") - self.repo.delete(todo) - return {"message": "Todo deleted successfully"} + deleted = self.repo.delete(todo) + return deleted diff --git a/app/services/user_service.py b/app/services/user_service.py index 50e4919..497aef4 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -15,8 +15,8 @@ def create_user(self, username: str, hashed_pw: str): detail="Username already exists" ) - self.repo.create(username, hashed_pw) - return {"message": "User created successfully"} + create = self.repo.create(username, hashed_pw) + return create def get_user(self, user_id: int): user = self.repo.get_by_id(user_id) @@ -27,6 +27,13 @@ def get_user(self, user_id: int): ) return user + def updated_user(self, user_id: int, username: str, full_name: str,bio:str, profile:str): + user = self.get_user(user_id) + if not user: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="user not found") + updated = self.repo.update(user_id,username, full_name, bio,profile) + return updated + def delete_user(self, user_id: int): user = self.repo.get_by_id(user_id) if not user: @@ -35,5 +42,5 @@ def delete_user(self, user_id: int): detail="User not found" ) - self.repo.delete(user) - return {"message": "User deleted successfully"} + deleted = self.repo.delete(user) + return deleted