From 8821782eecaf8e780e29dfd4dd1ac2dc27f41e93 Mon Sep 17 00:00:00 2001 From: Johan Date: Wed, 17 Dec 2025 09:40:11 +0100 Subject: [PATCH] Initial commit --- src/app/__init__.py | 0 src/app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 171 bytes src/app/__pycache__/main.cpython-313.pyc | Bin 0 -> 1849 bytes src/app/core/__init__.py | 0 .../core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 176 bytes .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 887 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 0 -> 2261 bytes src/app/core/config.py | 15 +++++ src/app/core/exceptions.py | 24 +++++++ src/app/graphql/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 179 bytes .../__pycache__/extensions.cpython-313.pyc | Bin 0 -> 1204 bytes .../__pycache__/mutations.cpython-313.pyc | Bin 0 -> 983 bytes .../__pycache__/queries.cpython-313.pyc | Bin 0 -> 1159 bytes src/app/graphql/extensions.py | 12 ++++ src/app/graphql/inputs/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 186 bytes .../ingredient_input.cpython-313.pyc | Bin 0 -> 580 bytes .../__pycache__/recipe_input.cpython-313.pyc | Bin 0 -> 728 bytes src/app/graphql/inputs/ingredient_input.py | 6 ++ src/app/graphql/inputs/recipe_input.py | 9 +++ src/app/graphql/mutations.py | 16 +++++ src/app/graphql/queries.py | 21 ++++++ src/app/graphql/resolvers/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 189 bytes .../recipe_resolvers.cpython-313.pyc | Bin 0 -> 1707 bytes src/app/graphql/resolvers/recipe_resolvers.py | 30 +++++++++ src/app/graphql/types/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 185 bytes .../__pycache__/ingredient.cpython-313.pyc | Bin 0 -> 565 bytes .../types/__pycache__/recipe.cpython-313.pyc | Bin 0 -> 752 bytes src/app/graphql/types/ingredient.py | 6 ++ src/app/graphql/types/recipe.py | 10 +++ src/app/main.py | 43 ++++++++++++ src/app/models/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 178 bytes .../__pycache__/ingredient.cpython-313.pyc | Bin 0 -> 529 bytes .../models/__pycache__/recipe.cpython-313.pyc | Bin 0 -> 687 bytes src/app/models/ingredient.py | 5 ++ src/app/models/recipe.py | 9 +++ src/app/repositories/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 184 bytes .../recipe_repository.cpython-313.pyc | Bin 0 -> 2488 bytes src/app/repositories/recipe_repository.py | 61 ++++++++++++++++++ src/app/services/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 180 bytes .../recipe_service.cpython-313.pyc | Bin 0 -> 1844 bytes src/app/services/recipe_service.py | 40 ++++++++++++ 48 files changed, 307 insertions(+) create mode 100644 src/app/__init__.py create mode 100644 src/app/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/__pycache__/main.cpython-313.pyc create mode 100644 src/app/core/__init__.py create mode 100644 src/app/core/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/core/__pycache__/config.cpython-313.pyc create mode 100644 src/app/core/__pycache__/exceptions.cpython-313.pyc create mode 100644 src/app/core/config.py create mode 100644 src/app/core/exceptions.py create mode 100644 src/app/graphql/__init__.py create mode 100644 src/app/graphql/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/graphql/__pycache__/extensions.cpython-313.pyc create mode 100644 src/app/graphql/__pycache__/mutations.cpython-313.pyc create mode 100644 src/app/graphql/__pycache__/queries.cpython-313.pyc create mode 100644 src/app/graphql/extensions.py create mode 100644 src/app/graphql/inputs/__init__.py create mode 100644 src/app/graphql/inputs/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/graphql/inputs/__pycache__/ingredient_input.cpython-313.pyc create mode 100644 src/app/graphql/inputs/__pycache__/recipe_input.cpython-313.pyc create mode 100644 src/app/graphql/inputs/ingredient_input.py create mode 100644 src/app/graphql/inputs/recipe_input.py create mode 100644 src/app/graphql/mutations.py create mode 100644 src/app/graphql/queries.py create mode 100644 src/app/graphql/resolvers/__init__.py create mode 100644 src/app/graphql/resolvers/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/graphql/resolvers/__pycache__/recipe_resolvers.cpython-313.pyc create mode 100644 src/app/graphql/resolvers/recipe_resolvers.py create mode 100644 src/app/graphql/types/__init__.py create mode 100644 src/app/graphql/types/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/graphql/types/__pycache__/ingredient.cpython-313.pyc create mode 100644 src/app/graphql/types/__pycache__/recipe.cpython-313.pyc create mode 100644 src/app/graphql/types/ingredient.py create mode 100644 src/app/graphql/types/recipe.py create mode 100644 src/app/main.py create mode 100644 src/app/models/__init__.py create mode 100644 src/app/models/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/models/__pycache__/ingredient.cpython-313.pyc create mode 100644 src/app/models/__pycache__/recipe.cpython-313.pyc create mode 100644 src/app/models/ingredient.py create mode 100644 src/app/models/recipe.py create mode 100644 src/app/repositories/__init__.py create mode 100644 src/app/repositories/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/repositories/__pycache__/recipe_repository.cpython-313.pyc create mode 100644 src/app/repositories/recipe_repository.py create mode 100644 src/app/services/__init__.py create mode 100644 src/app/services/__pycache__/__init__.cpython-313.pyc create mode 100644 src/app/services/__pycache__/recipe_service.cpython-313.pyc create mode 100644 src/app/services/recipe_service.py diff --git a/src/app/__init__.py b/src/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/__pycache__/__init__.cpython-313.pyc b/src/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66526f97eb90a13acc5f7ff76038a448e713e05d GIT binary patch literal 171 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABz4Qn*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM t#KgyE=4F<|$LkeT-r}&y%}*)KNwq6t1sV;qx){Xx$jr#dSi}ru0RRt}D-Qqw literal 0 HcmV?d00001 diff --git a/src/app/__pycache__/main.cpython-313.pyc b/src/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1451916680e1b72064e93d69c273d812a917b44 GIT binary patch literal 1849 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 F{{UKn`}_a^ literal 0 HcmV?d00001 diff --git a/src/app/core/config.py b/src/app/core/config.py new file mode 100644 index 0000000..146e283 --- /dev/null +++ b/src/app/core/config.py @@ -0,0 +1,15 @@ +from typing import List +from pydantic import AnyHttpUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + +class Settings(BaseSettings): + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + case_sensitive=True + ) + + PROJECT_NAME: str = "Recipe Book GraphQL API" + BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] + +settings = Settings() \ No newline at end of file diff --git a/src/app/core/exceptions.py b/src/app/core/exceptions.py new file mode 100644 index 0000000..0db2f2e --- /dev/null +++ b/src/app/core/exceptions.py @@ -0,0 +1,24 @@ +class BaseAppException(Exception): + """Exception de base pour l'application.""" + pass + +class DALException(BaseAppException): + """Exception levée pour les erreurs de la couche d'accès aux données (DAL).""" + def __init__(self, message: str, original_exception: Exception = None): + self.message = message + self.original_exception = original_exception + super().__init__(self.message) + +class BLLException(BaseAppException): + """Exception de base pour les erreurs de la couche métier (BLL).""" + pass + +class NotFoundBLLException(BLLException): + """Levée lorsqu'une ressource n'est pas trouvée.""" + def __init__(self, resource_name: str, resource_id: int | str): + message = f"{resource_name} avec l'ID '{resource_id}' non trouvé." + super().__init__(message) + +class ValidationBLLException(BLLException): + """Levée pour les erreurs de validation des règles métier.""" + pass \ No newline at end of file diff --git a/src/app/graphql/__init__.py b/src/app/graphql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/graphql/__pycache__/__init__.cpython-313.pyc b/src/app/graphql/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4cd0ec2797d4931c5a4c72c89d17392abcfe6b99 GIT binary patch literal 179 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}su*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM u#K8FR@tJv-#O@n03sVl>}8Plzs-E?MyttY*R z#~v(r^eTApPcbMY40`h9O^n`slVlq#K882%y?O7=%BJt+<=LT~ zSpV;AYpa;BSqC@)2GqdROV8)F5;Tn-l8+3T@}ep;$K@Q`V_s7cMZGAD*4^I^KXe1y3&;r{84MC$0byiZ8=3p5Cw^$4dL zcN^Qgfg`x*xQ&LB_`@?~8~b?pkaD)$WFn?4tjbFX0{?kW*?)5tbs|9Z0y>$+cE)OD ztac{%C6jwy=q#_amoK-LFTXB*omqUF{jk=ax%zqLYG)htWyUsG@@-^KmlJ(SE5K8Kc^ zptK%Un*pLHCn?=+xR&VzDM0S=KkYm%#Z?p8rS%O zQu9iwSJqRCKcU|9EBK?0rZ-7HNv)Ug_hm%P-Di)Ht^Mo}SFR|NClU0l7jcFwgFh~M u@1fuQ-yP-Hjs7)m@^e_M{;XI;^;0K=d;{wnto=@nkxlZ>`~zz4o#QWCR2=>Q literal 0 HcmV?d00001 diff --git a/src/app/graphql/__pycache__/mutations.cpython-313.pyc b/src/app/graphql/__pycache__/mutations.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caae63f60079a153866787733bd02d52360fceb6 GIT binary patch literal 983 zcmYjQO-tNB7@mA?Tvvb^tjFcwMiC;Ydj^cxvrMp^(qL~ zq0+A1NcN$v6y_caqKqM$-VDQ%Y$sVR&@&5+r*V%_KMX01@`9?%Xt|qu3+Xvd0*XsO zbrsN_eNt7hKyscOb$~_pa`Wr7bsk#uqaLZE~B)l zQ`$|#JeIgY=}GR#RpbGs2N7@)W9uZvaE;P14P*@$;D6d@0R6%^h*%tgR1+qp(a;oF z>nBNCO2M(@5v5$Rfcio}v@Z`&DM*2b_>#hXyWxH047hh3Gf4Yut7|X3-F|T7Lw6U_ zV-^VRbywGD$hs*mhu~*NC$YyN@cb94=FBhKUweOuW|;7JT-cs~S;R#X_i(B`9MR7hcgk<7Jn#Jda3sIL zah#4A=(5Su{jBjt-6h^RMq(Da+@(B$RH+>4iELxNE^q7&JVT0qBjFC3fu<EpcnTpQ5RVi=i2-r2Sh>zNb>-O3tPRQq zwWq?33;%)gH}VVMLS%$EaN?FK5?qPecW%Q@-AF9e}ZTHUW$8Q(jSVN+y+Mqy;bDt|K zpDHW;sk8m*%NSy4ySHE8ef#udaqg#xfm7oTR3~8u?L>>Zq3gFYUebG?#NKM7-fD(^b literal 0 HcmV?d00001 diff --git a/src/app/graphql/extensions.py b/src/app/graphql/extensions.py new file mode 100644 index 0000000..648c282 --- /dev/null +++ b/src/app/graphql/extensions.py @@ -0,0 +1,12 @@ +from strawberry.extensions import Extension +from app.core.exceptions import BLLException + +class BusinessLogicErrorExtension(Extension): + def on_request_end(self): + for error in self.execution_context.errors: + original_error = error.original_error + if isinstance(original_error, BLLException): + error.message = f"[Business Error] {original_error}" + if not error.extensions: + error.extensions = {} + error.extensions['code'] = original_error.__class__.__name__ \ No newline at end of file diff --git a/src/app/graphql/inputs/__init__.py b/src/app/graphql/inputs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/graphql/inputs/__pycache__/__init__.cpython-313.pyc b/src/app/graphql/inputs/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cd4ae9acd990edd49eb388b2adaa01cf79f100a GIT binary patch literal 186 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}s&*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM z#K8EOc?G3FmGSYJd6^~g@p=W7w>WHa^HWN5QtgUZfi{6$QVe2zWM*V!EMf+-08BP9 AXaE2J literal 0 HcmV?d00001 diff --git a/src/app/graphql/inputs/__pycache__/ingredient_input.cpython-313.pyc b/src/app/graphql/inputs/__pycache__/ingredient_input.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9eb485562ce59acaed18b97d955b173a94e8909 GIT binary patch literal 580 zcmYLFPiqu06n~kSZI`ucouVKfg(9mUWlJw21@W|qr8syp5YpMWO_|9|U$V#?i=dyN zAHt90Nr{(&r`{@a@aoI#tPSM7-=CM4ytL!-2xvXKc%?s3{vF7A4SLWX61ap6GWG@# zIJ}G2CAaw@JccZK1X+C0y9d3A(Sl8q>%flk+XDb`PuiyhE~ziF3^2+V#=F21KFFdc z)l?U`Ainrmg;%+yPF9a{Os$Wr;?`BECEUGoi&CYydJM39B>BO%Ia zzAOUX5`xN#-q{x7qeklrZIr3Vj;%vasx+o@(&@@rQXdFm1IvYU4)w{>DUxwWcek{` z%hxaXd#lj$vqE9jO!uby{J6onovGYeUhYlBT$L521Fdd2SiVCi?3{*5BGa|0CG7^0m&fp%^F0RtnGx&QzG literal 0 HcmV?d00001 diff --git a/src/app/graphql/inputs/__pycache__/recipe_input.cpython-313.pyc b/src/app/graphql/inputs/__pycache__/recipe_input.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc5311e04ad7e40e7ee5b91f26f14c5b5803b7b4 GIT binary patch literal 728 zcmYLH&1(}u6rb6TBqsfECBf2D5o`&iQX;(+5sIfKBBhI-MuzR~m`>U3Zr@Ch97_*g zz4VXpZ}C(FEhB}3r`#$zcy-=xq67Qh`@QeqV`t0DO=92n#ew{4^)CdA=g*DVJ8N8# zkc4zV0JSrqeFvNk61t%`aQhy38-$VLve%-#(re@i3H@~v*7lsrvo}wG+K_hZQ$%mN z)OfF@(o<@Y1*?bI5JVzHrVq1XtmAo2soXi3?OWr@&IKA0a6$_1Q^Gvvht8Ibl$Kjs zHC9J5HsT=;eP|48-MU#hNRnd_%R-b>rkG@WEp;kPD-kLNS!kJOrX~OFqrTX6AD9N` zSu_%yn139J`#Q4a}$jDg43}gWS D{fIKH literal 0 HcmV?d00001 diff --git a/src/app/graphql/resolvers/__pycache__/recipe_resolvers.cpython-313.pyc b/src/app/graphql/resolvers/__pycache__/recipe_resolvers.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36da5173a3185c8ac53da631313617544defcf9a GIT binary patch literal 1707 zcmahJO>f*p^o_rEvraZ`b^}RTwWi&Qx+&W#q)4sWpr}324Ij~3DH3tDa@Nz-spH+w zcvCj&A>80pafnDyzzMh^!J)sP=OiE{7*&;k3%7-Y12^7y?WBn+@uYd*p7-W`c&D!~ z2MD8|OnRpkfZv6Xf|@WpYsjpE1s0hCCa5$;DiV__GLsD<%Tr24WopbTQ(7g%GBHo4 zvXve#8Bocw9BNcsvQ=^IxMXP~u%IyA%GgjHN<;2b^pz7|#mdIMeO8a1LA^SzSh-k} zpC_jNr}*f7Q;L*#Jsw8c51OIZ2pr!ekyfR3uSt>1Pr7bbxEVBCVWcq{wpdW#4+tQk zAmiCNk7yNU_iR;RIRvNRQlcq=NtWNLQr_^FC^MRk7Bd=iAq_lc_>SRYQq*v%5jI*O z<%W-k(K-!LC}Y-n6fnmcr)bBEt+NsQvM7f;=-cyvD?WjMI z(&#LYu`_u1QA8{7hd#Weo141%lRmnxjIv&ln&i24f=mjc!*L<6BJ=-+oJ|J}X5dnC zk3}vMEUdKr#>)%a3TRqIqv$B{>&mrmB~(vL z*MVYa_*Gntr^u}06RSuMLji6PE?odF72uuY00EgHFPl2l;+7r;iAe#GIv5a0 z0htw1W2k0RZl9hKS;Tw4uM-?<#X}FljATidSm}k`WXb^HU!;WDh>{Wrp z|DoHkbAxi4FsJ zxQl27Iyoo~Zxu?Lh0=~H4{5&@N}pcu$S^SY#n9(NU-54qe*N(0?D#K*Qbz@Kumk9` zn~Nvo?52KpJ70Ko@3VW`ct`QfM)AycvGi9)>D4-*sM^1O>*&qJ)IT~sc|tM!qZ|*J z^I(oL){eBW-4ywYl$q#uG=%$GPWHEqR{U`!S&KWOlP{X%0Zi?+SYl!rqGQ4<`N{0r z)1OW@HN;hpmp+ni-iT*EE)CN~Hex!utCGFC*l=4uonWt{PV5lBg{UJFLVks#&*8Oa zF!~IxJcsdh81E<&Iloq1yYu+?E+E^<$z)>fqc3kizKxY56W@OPMBBLV_O9SNy}d+R P3p#-0`&a%EB#!$Z+S{4F literal 0 HcmV?d00001 diff --git a/src/app/graphql/resolvers/recipe_resolvers.py b/src/app/graphql/resolvers/recipe_resolvers.py new file mode 100644 index 0000000..8ba0bdd --- /dev/null +++ b/src/app/graphql/resolvers/recipe_resolvers.py @@ -0,0 +1,30 @@ +import strawberry +from typing import List, Optional +from app.graphql.types.recipe import Recipe +from app.graphql.inputs.recipe_input import AddRecipeInput +import app.services.recipe_service as recipe_service + +def resolve_recipes() -> List[Recipe]: + """Resolver pour obtenir la liste de toutes les recettes.""" + return recipe_service.get_all_recipes() + + +def resolve_recipe_by_id(id: strawberry.ID) -> Optional[Recipe]: + """Resolver pour obtenir une recette par son ID.""" + # Strawberry.ID est une chaîne, il faut la convertir en entier + return recipe_service.get_recipe_by_id(recipe_id=int(id)) + + +def resolve_add_recipe(input: AddRecipeInput) -> Recipe: + """Resolver pour ajouter une nouvelle recette.""" + # Conversion des inputs strawberry en dictionnaires simples + ingredients_data = [ + {"name": ing.name, "quantity": ing.quantity} + for ing in input.ingredients + ] + + return recipe_service.add_new_recipe( + title=input.title, + description=input.description, + ingredients=ingredients_data + ) \ No newline at end of file diff --git a/src/app/graphql/types/__init__.py b/src/app/graphql/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/graphql/types/__pycache__/__init__.cpython-313.pyc b/src/app/graphql/types/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b65fc730eef329b7e1469fa5ec9248e211960093 GIT binary patch literal 185 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}so*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM z#K8C^l?AEAG4b)4d6^~g@p=W7w>WHa^HWN5QtgUZffj)rQVe2zWM*V!EMf+-01>h< A`2YX_ literal 0 HcmV?d00001 diff --git a/src/app/graphql/types/__pycache__/ingredient.cpython-313.pyc b/src/app/graphql/types/__pycache__/ingredient.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f13a3f3db7f84ace3ffed2747594fd201a05f8e1 GIT binary patch literal 565 zcmYLFO-md>5Urk>HOs_RHzW{p5fqV)Aj^su2|~z8BqABSgodV_?W`j+J!5q*VU8li zU*M0(KMI|T;K`d|4!Nd!cQ!Uu)qC~oqr014F9te0*B`SpiVvPFPNxOUA%QzcAYsRV zz~M`Ur&duKyR^F@1NZgQ z0Y5Ve4ZqA4mi1^j+T*8ndLgkm#qv_6*6?CD5@S`A6ecLEi>sU)Ou4KozTmd5l;PR` z=j~QCMzmn&{8P;`xm&o8dpAeFR{B3aq0}y>F5*;n%xpNm}{vN>vmIN9QZ~_YMBf`A@HNgD9d5mgS zM&#ZiFGm%2$4-fnlI4L9)j_#kG5&#wWF}@dXdKx$B`tL^Sz>a@}PGW_1y`r&QC+A^tKh84ud$7}84>_DdaZ-vEho-JBb zvI7sDHMUM%zlJVa{WiE5*{aVe!gN#lyFU6Ix`t(|Y2ZTxXO+PWcV<}Ps>zb(zL F`U^hxxsLz< literal 0 HcmV?d00001 diff --git a/src/app/graphql/types/ingredient.py b/src/app/graphql/types/ingredient.py new file mode 100644 index 0000000..793401e --- /dev/null +++ b/src/app/graphql/types/ingredient.py @@ -0,0 +1,6 @@ +import strawberry + +@strawberry.type +class Ingredient: + name: str + quantity: str \ No newline at end of file diff --git a/src/app/graphql/types/recipe.py b/src/app/graphql/types/recipe.py new file mode 100644 index 0000000..69c796e --- /dev/null +++ b/src/app/graphql/types/recipe.py @@ -0,0 +1,10 @@ +import strawberry +from typing import List +from .ingredient import Ingredient + +@strawberry.type +class Recipe: + id: strawberry.ID + title: str + description: str + ingredients: List[Ingredient] \ No newline at end of file diff --git a/src/app/main.py b/src/app/main.py new file mode 100644 index 0000000..60e72f0 --- /dev/null +++ b/src/app/main.py @@ -0,0 +1,43 @@ +from fastapi import FastAPI +from app.core.config import settings +from strawberry.fastapi import GraphQLRouter +import strawberry +import uvicorn +from fastapi.middleware.cors import CORSMiddleware +from app.graphql.extensions import BusinessLogicErrorExtension +from app.graphql.mutations import Mutation +from app.graphql.queries import Query + +# Crée l'application FastAPI +app = FastAPI( + title="Recipe Book GraphQL API", + description="API GraphQL pour un livre de recettes de cuisine.", + version="1.0.0" +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin).rstrip('/') for origin in settings.BACKEND_CORS_ORIGINS] if settings.BACKEND_CORS_ORIGINS else ["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Crée le schéma GraphQL +schema = strawberry.Schema( + query=Query, + mutation=Mutation, + extensions=[BusinessLogicErrorExtension] +) + +# Crée le routeur GraphQL +graphql_app = GraphQLRouter(schema) +app.include_router(graphql_app, prefix="/graphql") + +@app.get("/", tags=["Root"]) +def read_root(): + return {"message": "Bienvenue sur l'API du livre de recettes. Rendez-vous sur /graphql"} + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8004) \ No newline at end of file diff --git a/src/app/models/__init__.py b/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/models/__pycache__/__init__.cpython-313.pyc b/src/app/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fa11c82896aebe6605f838dc994c46cfea8cc6fd GIT binary patch literal 178 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}s$*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM z#N_6uq~;XI#K&jmWtPOp>lIYq;;;beR)pc=O6brS7QVO+93nD9CL<+4ZTNEMm76@@BT}Ngz<4g81M-lYe zi$9|ONCG{1@+Pd_`?9k(koUgtecwy+G93;FKqA=3u%(;08Ew_5;2% zWbfcLyayh=0Uiss6L&t0#!1?{46^0pu+DXvb#fQyUYLF&egguKalnW(j6Z-# zM2|-wNtvNIAfDjDxR);Y!GRg0&I@haeK^G@MpfGP&nL>b!nJ?kC|&e@DMMLGKajGj z^GO--vXo>i=gLbde-&t5Sr~0vps?+7`!ccB-&C#`;WW&WdVlBy(NWl*Xu<)ALeT%!FzhLC@2r z6~+I#Hk%g1r_Vx|(JoWChTFmL^5FKx%GDvRJj+LE?E6idhv72}=B_n7uXC*u^8LN; e6TaXYQAXPa@DMY`{=(bGBx0+-*PnpWtm_}KlZZC} literal 0 HcmV?d00001 diff --git a/src/app/models/__pycache__/recipe.cpython-313.pyc b/src/app/models/__pycache__/recipe.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48ce25c1193e3251aa6910e71fb79785f80fe8e6 GIT binary patch literal 687 zcmYLH&ubGw6rR~%*=}ql(9%*=+JHz9q(r=wQt$_eiPqxcK^Pd;-7%f8+1a`Kd$5WV{*o7RDS@B8L^^X9!byI5b32(J4dAIop(e_3+1{1sRn zBe*0niD{qoD9}BNoxam^!A0!GUf=8a;O~(g(k8LLOX7ghM{a!{fMJ?vL?-0C zNJfHl6LCH&vvF?m4bIQUNxo8Ua{fVr(zz@|QKIb1LL1yoilWqs&8+OM(f}S_4}q;= zi*seglqXt)d_UF#qhJHI1%AZ5etO8>DglZOa{=X~d(eHzPABPE0;5wXha%OAjSjjz z6QdH{0FvtLJZB10mQ)qPYDKOXEc<>?O(4XqOJirCZln1`reX8q`Lwxl(VMn5e|VeI z*4CHd_tySTZy&a=;!ipa6IPQfDX?6yuVVr|sqC<9KPLJ=wqke!G;?8M!kf+cVPs<9W3G`Q2ZFZh7_}lO3|5 literal 0 HcmV?d00001 diff --git a/src/app/models/ingredient.py b/src/app/models/ingredient.py new file mode 100644 index 0000000..07ca891 --- /dev/null +++ b/src/app/models/ingredient.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class Ingredient(BaseModel): + name: str + quantity: str \ No newline at end of file diff --git a/src/app/models/recipe.py b/src/app/models/recipe.py new file mode 100644 index 0000000..8648ab1 --- /dev/null +++ b/src/app/models/recipe.py @@ -0,0 +1,9 @@ +from pydantic import BaseModel +from typing import List +from .ingredient import Ingredient + +class Recipe(BaseModel): + id: int + title: str + description: str + ingredients: List[Ingredient] \ No newline at end of file diff --git a/src/app/repositories/__init__.py b/src/app/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/repositories/__pycache__/__init__.cpython-313.pyc b/src/app/repositories/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f8683ea26e0a3bf23f5d60941fefd82d74ed83b GIT binary patch literal 184 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}s?*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM z#1y3#D@Lnjq^9X9n?^m| z^=nM9>NGqm(aXKrFogk^D!EmSEyo$ zb@`GyuTc&77*?=~HQa;sl8oax5sgWlipDhV72~*qGbIIQaUXsQAHe;QEQb$v_vE87 zjt8Pq!-vE;t_q!TnfBnp6@37WAUrgP@Zo8>VV8O#D(thn`>Px4u46kSWQ3Kt_JKtmk8ovjy1ZPJx#C$h!u6HV@&YGl zaP<=sE-@}mfpwM>aCN#|uDM*rp-oAcgF7b;J9J!kqe3$5N^p&mAf%oxl4S4GfDx|l-T-nBUE5nn2hdfC$>WI0 z2hfE>2zk=AZjvIjpzLqkXGYMXG~GSEFgRlm97Rv5J_R=r-Kdr@`> zgNdjmbAPQ-syDdeit@|74oXQZkAe~G2#{6u%j6%oGBXd4H~;*l_BeZFGkavs`YC(t zyW_3Q%p-l~f18;ZCZhMbF{bc%_a>)m@5dHoLLOmq9hg@!xljNkKK(5&FOQ$b0w*JPHv$%W^Sx=j~6aAKoU0NTF`J7eg3$z%c z!dH462v2sc2S_X?>@^I79utzplH*m)8x0eReCUmcy%4?!o7p&!RrKr7=-o@(nZf&0 zt&FjD@yE=GC;8#W`LWIXSo7Le{`6+{^fMJ5KHAn$wtp8?5Iq6pmADBT#DDkiK%mu2 zFS5BTElTGlPkt#zB7m=bi~A8WUaABvpGYILg06{LiM|DWk9#pf{_krSAW&f$WYC4z zJ8|_ygk7lFy#N#FPDia{axLTBY$w!IoL*G9ZhGXi0Aff1bi_5tQ{q*lil8Fd z_P`Kv;0cfX4Kh|F?yFd6(21dDc>WC>Is)-9uvs9hsI8-P-7*dy6yDd7z0`(YbBF8i zcOpAO=RiU9UiLMRc1)6_|DdrgH1;zpv=bLN&d)#14L47Ft-aVJ@_~rwlizVct$^ nLOBV_hXyk8YO+0rQt4IwH*G+gc!mTyg>px>`bS%+FN)UhIKp_+ literal 0 HcmV?d00001 diff --git a/src/app/repositories/recipe_repository.py b/src/app/repositories/recipe_repository.py new file mode 100644 index 0000000..b28e9f6 --- /dev/null +++ b/src/app/repositories/recipe_repository.py @@ -0,0 +1,61 @@ +from typing import List, Optional, Dict, Any +from app.models.recipe import Recipe + +# --- Base de données en mémoire --- +_RECIPES_DB = [ + { + "id": 1, + "title": "Crêpes simples", + "description": "Une recette facile pour des crêpes délicieuses.", + "ingredients": [ + {"name": "Farine", "quantity": "250g"}, + {"name": "Oeufs", "quantity": "4"}, + {"name": "Lait", "quantity": "500ml"}, + {"name": "Sucre", "quantity": "2 cuillères à soupe"} + ], + }, + { + "id": 2, + "title": "Gâteau au chocolat", + "description": "Un classique qui plaît à tout le monde.", + "ingredients": [ + {"name": "Chocolat noir", "quantity": "200g"}, + {"name": "Beurre", "quantity": "150g"}, + {"name": "Sucre", "quantity": "100g"}, + {"name": "Oeufs", "quantity": "3"}, + {"name": "Farine", "quantity": "50g"} + ], + }, +] +# Compteur pour simuler l'auto-incrémentation des IDs +_next_id = 3 +# --- + +class RecipeRepository: + """ + Simule l'accès à une base de données de recettes. + """ + def list(self) -> List[Recipe]: + """Retourne toutes les recettes.""" + return [Recipe.model_validate(r) for r in _RECIPES_DB] + + def find_by_id(self, recipe_id: int) -> Optional[Recipe]: + """Trouve une recette par son ID.""" + for recipe_data in _RECIPES_DB: + if recipe_data["id"] == recipe_id: + return Recipe.model_validate(recipe_data) + return None + + def create(self, recipe_data: Dict[str, Any]) -> Recipe: + """Crée une nouvelle recette et l'ajoute à la DB.""" + global _next_id + new_recipe = { + "id": _next_id, + **recipe_data + } + _RECIPES_DB.append(new_recipe) + _next_id += 1 + return Recipe.model_validate(new_recipe) + +# Instance unique du repository +recipe_repository = RecipeRepository() diff --git a/src/app/services/__init__.py b/src/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/app/services/__pycache__/__init__.cpython-313.pyc b/src/app/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ca81f8c3a2755087146ee0aff3ac92df96d04e4 GIT binary patch literal 180 zcmey&%ge<81lo;`nIQTxh=2h`DC08=kTI1Zok5e)ZzV$!6Oi{ABy}s;*(xTqIJKxa zCMzekD8JIkz{ofzpfWilu_!m7C_gJTxuiHI*T5(~B{er6NTnAg7GxCW#1t1L$0QaM z!~nIHWhSQ<$Hd2H=4F<|$LkeT-r}&y%}*)KNwq6t1=;~}LotZ)k(rT^v4|PS0s!=e BF4zD7 literal 0 HcmV?d00001 diff --git a/src/app/services/__pycache__/recipe_service.cpython-313.pyc b/src/app/services/__pycache__/recipe_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83b7e299162f354d90f61d7af5ac0ef2d7123cfc GIT binary patch literal 1844 zcmah}&2QX96d!-=W@BeJ*|a33Dyq8)9~Q_8Aw7UlL};nR?ozdDHqgXqW$a1n$ZMM! zJFvIqKj0E6Ikm?P;=qw((Oz=gG>24HOAm12wkV-z-gwtb8VT{_nR)N|J^Rh?eVpX; z5(0Yr`xWQQ3_`y%A*JylbMgZ)J4i=5S3zZtaAltGWq}9^)A1FtoFSQn7b@BE7#T}= zv63r~lku`dB#<)(XJp_9Xo=Uem(i9;WPQw-0DWdj)N_d@ua6rAU4kWpHE$FQVTorx zY$cXCeIl`(+{~+mKS7IBAr`9+4P)`9V}-G>;r`cN~ER4%#-Lan=RMM1YiuoC)Fh^gYjf>?0~f~N9h46I_&g{$qO6XqV@yYYch zr9w*EuFzI@^A{C z5|~7xzNdV8QJS9s7Lvw_O0LK;?qLu=Sz=ThlRVn+}$T~vHjTq{Y>bDE(B#`Y7u8p z!Ls9R5^Ouz3#r9&kdg>Yl9Qi+*+F%b;%vTxK&qY1-_R#5+0@d8K-Wj#)VVZkkQ4qt zZ1^)as1A0zpb4I^&Z0@E#(+LYB~%wuKghKhEr5Zvvco zb+0Bx8aP#YtU{M{mHhLkUiQ;bNfxIU_$nWK!7Up}e*N+OTz1-@HESi{2 z6!(q_YrWiBKZ~YkpUjrv`D;wPkZo5^C(y)e$MW1`dG2B5NM7iP3rSVoR;S`@*b3MU zS*{6-iLgG5i!eg8rf*}HYJ;egHi=72Kbo!%46osxM!MDjgbU2j@(I>Py-1Uu834s5 zPAoWHNLW|I0$`ZjndDgrY~T!V$$bbxL@r(?Z-9;!F5Q5wFK`_99F-1H=^2`ThCX_Z zs(q2?=65gdZtPt-MZor@95?+?=p#5f*;97(XQsJ#cjx*Dj?Rtb_{Dy-bRZn)2f1ID S50`6+uJ^@Pe>0OLj(-4TT*jjS literal 0 HcmV?d00001 diff --git a/src/app/services/recipe_service.py b/src/app/services/recipe_service.py new file mode 100644 index 0000000..87036b2 --- /dev/null +++ b/src/app/services/recipe_service.py @@ -0,0 +1,40 @@ +from typing import List, Dict, Any +from app.models.recipe import Recipe +from app.repositories.recipe_repository import recipe_repository +from app.core.exceptions import NotFoundBLLException, ValidationBLLException + +def get_all_recipes() -> List[Recipe]: + """Récupère toutes les recettes.""" + return recipe_repository.list() + + +def get_recipe_by_id(recipe_id: int) -> Recipe: + """ + Récupère une recette par son ID. + Lève une exception si la recette n'est pas trouvée. + """ + recipe = recipe_repository.find_by_id(recipe_id) + if not recipe: + raise NotFoundBLLException(resource_name="Recipe", resource_id=recipe_id) + return recipe + + +def add_new_recipe(title: str, description: str, ingredients: List[Dict[str, Any]]) -> Recipe: + """ + Ajoute une nouvelle recette après validation. + """ + # Règle métier simple : le titre ne doit pas être vide + if not title or not title.strip(): + raise ValidationBLLException("Le titre de la recette ne peut pas être vide.") + + # Règle métier simple : il faut au moins un ingrédient + if not ingredients: + raise ValidationBLLException("Une recette doit contenir au moins un ingrédient.") + + recipe_data = { + "title": title, + "description": description, + "ingredients": ingredients + } + + return recipe_repository.create(recipe_data) \ No newline at end of file